QTube 2.2.0__py3-none-any.whl → 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
QTube/scripts/qtube.py CHANGED
@@ -127,7 +127,7 @@ def main():
127
127
  ["internal"],
128
128
  )
129
129
  QTube.utils.helpers.print2(
130
- f"The following verbosity options are enabled: {verb}.\n",
130
+ f"The following verbosity options are enabled: {', '.join(verb)}.\n",
131
131
  fancy,
132
132
  "info",
133
133
  ["internal"],
@@ -383,6 +383,8 @@ def main():
383
383
  vid_dicts = partial["items"]
384
384
  responses["items"].extend(vid_dicts)
385
385
 
386
+ video_IDs_lst = [vid["id"] for vid in responses["items"]]
387
+
386
388
  # Titles retrieving
387
389
  titles = QTube.utils.youtube.videos.get_titles(response=responses)
388
390
 
@@ -390,7 +392,9 @@ def main():
390
392
  durations = QTube.utils.youtube.videos.get_durations(response=responses)
391
393
 
392
394
  # Shorts retrieving
393
- shorts = QTube.utils.youtube.videos.is_short(response=responses)
395
+ shorts = QTube.utils.youtube.videos.is_short(
396
+ response=responses, video_IDs=video_IDs_lst
397
+ )
394
398
 
395
399
  # Languages retrieving
396
400
  languages = QTube.utils.youtube.videos.get_languages(response=responses)
@@ -410,6 +414,33 @@ def main():
410
414
  # Live status retrieving
411
415
  live_statuses = QTube.utils.youtube.videos.is_live(response=responses)
412
416
 
417
+ # View counts retrieving
418
+ view_counts = QTube.utils.youtube.videos.get_view_counts(response=responses)
419
+
420
+ # Like counts retrieving
421
+ like_counts = QTube.utils.youtube.videos.get_like_counts(response=responses)
422
+
423
+ # Comment counts retrieving
424
+ comment_counts = QTube.utils.youtube.videos.get_comment_counts(response=responses)
425
+
426
+ # Likes/views ratio retrieving
427
+ likes_to_views_ratio = QTube.utils.youtube.videos.get_likes_to_views_ratio(
428
+ like_counts, view_counts
429
+ )
430
+
431
+ # Comments/views ratio retrieving
432
+ comments_to_views_ratio = QTube.utils.youtube.videos.get_likes_to_views_ratio(
433
+ comment_counts, view_counts
434
+ )
435
+
436
+ # Paid promotions retrieving
437
+ paid_advertising = QTube.utils.youtube.videos.has_paid_advertising(
438
+ response=responses
439
+ )
440
+
441
+ # Made for Kids retrieving
442
+ made_for_kids = QTube.utils.youtube.videos.is_made_for_kids(response=responses)
443
+
413
444
  # Resolutions retrieving (does not use YT API)
414
445
  lowest_resolution = user_params_dict.get("lowest_resolution")
415
446
  if lowest_resolution is not None:
@@ -469,6 +500,27 @@ def main():
469
500
  # Live statuses
470
501
  vid_info.update({"live status": live_statuses[index]})
471
502
 
503
+ # Views
504
+ vid_info.update({"views": view_counts[index]})
505
+
506
+ # Likes
507
+ vid_info.update({"likes": like_counts[index]})
508
+
509
+ # Comments
510
+ vid_info.update({"comments": comment_counts[index]})
511
+
512
+ # Likes/views
513
+ vid_info.update({"likes_to_views_ratio": likes_to_views_ratio[index]})
514
+
515
+ # Comments/views
516
+ vid_info.update({"comments_to_views_ratio": comments_to_views_ratio[index]})
517
+
518
+ # Paid promotions
519
+ vid_info.update({"has_paid_ad": paid_advertising[index]})
520
+
521
+ # Made for kids
522
+ vid_info.update({"made_for_kids": made_for_kids[index]})
523
+
472
524
  # Resolutions
473
525
  if lowest_resolution is not None:
474
526
  vid_info.update({"resolutions": resolutions[index]})
@@ -543,6 +595,12 @@ def main():
543
595
  lowest_definition = user_params_dict.get("lowest_definition")
544
596
  preferred_dimensions = user_params_dict.get("preferred_dimensions")
545
597
 
598
+ views_threshold = user_params_dict.get("views_threshold")
599
+ likes_threshold = user_params_dict.get("likes_threshold")
600
+ comments_threshold = user_params_dict.get("comments_threshold")
601
+ likes_to_views_ratio_threshold = user_params_dict.get("likes_to_views_ratio")
602
+ comments_to_views_ratio_threshold = user_params_dict.get("comments_to_views_ratio")
603
+
546
604
  # Duration filtering
547
605
  if min_max_durations is not None:
548
606
  for vid_info in videos.values():
@@ -621,6 +679,22 @@ def main():
621
679
  if vid_ID in new_vid_IDs:
622
680
  videos[vid_ID].update({"to add": False})
623
681
 
682
+ # Paid advertisement filtering
683
+ if user_params_dict["allow_paid_promotions"] is False:
684
+ for vid_ID, vid_info in videos.items():
685
+ if vid_info["to add"] is False:
686
+ continue
687
+ elif vid_info["has_paid_ad"]:
688
+ vid_info.update({"to add": False})
689
+
690
+ # Made for kids filtering
691
+ if user_params_dict["only_made_for_kids"] is True:
692
+ for vid_ID, vid_info in videos.items():
693
+ if vid_info["to add"] is False:
694
+ continue
695
+ elif not vid_info["made_for_kids"]:
696
+ vid_info.update({"to add": False})
697
+
624
698
  # Language filtering
625
699
  if preferred_languages is not None:
626
700
  preferred_languages.append("unknown")
@@ -771,6 +845,43 @@ def main():
771
845
  ):
772
846
  vid_info.update({"to add": False})
773
847
 
848
+ # Views filtering
849
+ if views_threshold > 0:
850
+ for vid_ID, vid_info in videos.items():
851
+ if vid_info["to add"] and vid_info["views"] < views_threshold:
852
+ vid_info.update({"to add": False})
853
+
854
+ # Likes Filtering
855
+ if likes_threshold > 0:
856
+ for vid_ID, vid_info in videos.items():
857
+ if vid_info["to add"] and vid_info["likes"] < likes_threshold:
858
+ vid_info.update({"to add": False})
859
+
860
+ # Comments filtering
861
+ if comments_threshold > 0:
862
+ for vid_ID, vid_info in videos.items():
863
+ if vid_info["to add"] and vid_info["comments"] < comments_threshold:
864
+ vid_info.update({"to add": False})
865
+
866
+ # Likes/views ratio filtering
867
+ if likes_to_views_ratio_threshold > 0:
868
+ for vid_ID, vid_info in videos.items():
869
+ if (
870
+ vid_info["to add"]
871
+ and vid_info["likes_to_views_ratio"] < likes_to_views_ratio_threshold
872
+ ):
873
+ vid_info.update({"to add": False})
874
+
875
+ # Comments/views ratio filtering
876
+ if comments_to_views_ratio_threshold > 0:
877
+ for vid_ID, vid_info in videos.items():
878
+ if (
879
+ vid_info["to add"]
880
+ and vid_info["comments_to_views_ratio"]
881
+ < comments_to_views_ratio_threshold
882
+ ):
883
+ vid_info.update({"to add": False})
884
+
774
885
  ## Selecting correct videos
775
886
  videos_to_add = {
776
887
  vid_ID: vid_info for vid_ID, vid_info in videos.items() if vid_info["to add"]
QTube/utils/checks.py CHANGED
@@ -334,6 +334,23 @@ def check_user_params(params_dict: dict) -> bool:
334
334
  isinstance(params_dict.get("ignore_premieres"), bool),
335
335
  # Fancy text
336
336
  isinstance(params_dict.get("fancy_mode"), bool),
337
+ # Views
338
+ isinstance(params_dict.get("views_threshold"), int)
339
+ and params_dict.get("views_threshold") >= 0,
340
+ # Likes
341
+ isinstance(params_dict.get("likes_threshold"), int)
342
+ and params_dict.get("likes_threshold") >= 0,
343
+ # Comments
344
+ isinstance(params_dict.get("comments_threshold"), int)
345
+ and params_dict.get("comments_threshold") >= 0,
346
+ # Likes/views ratio
347
+ isinstance(params_dict.get("likes_to_views_ratio"), (int, float))
348
+ and 0 <= params_dict.get("likes_to_views_ratio") <= 1,
349
+ # Comments/views ratio
350
+ isinstance(params_dict.get("comments_to_views_ratio"), (int, float))
351
+ and 0 <= params_dict.get("comments_to_views_ratio") <= 1,
352
+ # Paid promotions
353
+ isinstance(params_dict.get("allow_paid_promotions"), bool),
337
354
  ]
338
355
 
339
356
  ok = all(checks)
@@ -404,7 +421,7 @@ def check_version() -> tuple[str]:
404
421
  return version, latest_release
405
422
 
406
423
 
407
- def compare_software_versions(version1, version2):
424
+ def compare_software_versions(version1, version2) -> str:
408
425
  """Compare two software versions, using version2 as the reference against which version1 is compared.
409
426
 
410
427
  Args:
@@ -440,3 +457,18 @@ def compare_software_versions(version1, version2):
440
457
  return "older"
441
458
 
442
459
  return "same"
460
+
461
+
462
+ def check_URL_redirect(url: str, redirect_code: int) -> bool:
463
+ """Checks if the provided URL redirects to another page.
464
+
465
+ Args:
466
+ url (str): URL to check for redirection
467
+ redirect_code (int): Status code to check for (3xx)
468
+
469
+ Returns:
470
+ (bool): True if the URL redirects to another page with the correct status code, False otherwise.
471
+ """
472
+ r = requests.get(url)
473
+
474
+ return any([resp.status_code == redirect_code for resp in r.history])
QTube/utils/helpers.py CHANGED
@@ -7,7 +7,7 @@ from googleapiclient.errors import HttpError
7
7
  from colorama import Fore, Style
8
8
 
9
9
 
10
- def handle_http_errors(verbosity: list[str],fancy, func, *args, **kwargs):
10
+ def handle_http_errors(verbosity: list[str], fancy, func, *args, **kwargs):
11
11
  """Handles http errors when making API queries.
12
12
  If after 5 tries, the function could not be executed, it shuts the program down.
13
13
 
@@ -27,7 +27,11 @@ def handle_http_errors(verbosity: list[str],fancy, func, *args, **kwargs):
27
27
  try:
28
28
  res = func(*args, **kwargs)
29
29
  print2(
30
- f"{func.__name__} successfully executed.",fancy,"success", ["all", "func"], verbosity
30
+ f"{func.__name__} successfully executed.",
31
+ fancy,
32
+ "success",
33
+ ["all", "func"],
34
+ verbosity,
31
35
  )
32
36
  return res # Return the response if no error occurs
33
37
  except HttpError as err:
@@ -61,7 +65,7 @@ def handle_http_errors(verbosity: list[str],fancy, func, *args, **kwargs):
61
65
  sys.exit() # Exit the program after 5 retries
62
66
 
63
67
 
64
- def fancify_text(text, color, style, emoji):
68
+ def fancify_text(text, color, style, emoji) -> str:
65
69
  """Modifies the color and content of a string.
66
70
 
67
71
  Args:
@@ -100,9 +104,9 @@ def print2(
100
104
  elif fancy_type == "fail":
101
105
  print(fancify_text(message, Fore.RED, Style.BRIGHT, "❌"))
102
106
  elif fancy_type == "warning":
103
- print(fancify_text(message, Fore.YELLOW, Style.NORMAL, "⚠️"))
107
+ print(fancify_text(message, Fore.YELLOW, Style.BRIGHT, "⚠️"))
104
108
  elif fancy_type == "info":
105
- print(fancify_text(message, Fore.WHITE, Style.BRIGHT,"📢"))
109
+ print(fancify_text(message, Fore.WHITE, Style.BRIGHT, "📢"))
106
110
  elif fancy_type == "video":
107
111
  print(fancify_text(message, Fore.BLUE, Style.BRIGHT, "🎞️"))
108
112
  else:
@@ -157,7 +161,7 @@ def merge_dicts(list_of_dicts: list) -> dict:
157
161
  return {key: value for d in list_of_dicts for key, value in d.items()}
158
162
 
159
163
 
160
- def strip_emojis(text):
164
+ def strip_emojis(text) -> str:
161
165
  """Strips emojis from a string.
162
166
 
163
167
  Args:
@@ -186,7 +190,7 @@ def strip_emojis(text):
186
190
  return remove_multiple_spaces(clean_text)
187
191
 
188
192
 
189
- def strip_punctuation(text):
193
+ def strip_punctuation(text) -> str:
190
194
  """Strips punctuation from a string (all of these characters: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~).
191
195
 
192
196
  Args:
@@ -200,7 +204,7 @@ def strip_punctuation(text):
200
204
  return remove_multiple_spaces(clean_text)
201
205
 
202
206
 
203
- def make_lowercase(text):
207
+ def make_lowercase(text) -> str:
204
208
  """Converts all uppercase letters to lowercase from a string.
205
209
 
206
210
  Args:
@@ -213,7 +217,7 @@ def make_lowercase(text):
213
217
  return text.lower()
214
218
 
215
219
 
216
- def remove_multiple_spaces(text):
220
+ def remove_multiple_spaces(text) -> str:
217
221
  """Removes multiple spaces in a string and replaces them with a single space.
218
222
 
219
223
  Args:
@@ -223,3 +227,21 @@ def remove_multiple_spaces(text):
223
227
  (str): Same text string, but with multiple spaces replaced by a single space.
224
228
  """
225
229
  return re.sub(" +", " ", text)
230
+
231
+
232
+ def divide_lists(list1, list2, percentage: False) -> list[int | float]:
233
+ """Divides two python lists element-wise.
234
+
235
+ Args:
236
+ list1 (lst[int|float]): Dividend list.
237
+ list2 (lst[inf|float]): Divisor list.
238
+ percentage (bool): Determines if the result of the division is expressed as a percentage.
239
+
240
+ Returns:
241
+ res (lst[int|float]): List containing the results of the element-wise division.
242
+ """
243
+ if percentage:
244
+ res = [(a / b) * 100 if b != 0 else None for a, b in zip(list1, list2)]
245
+ else:
246
+ res = [a / b if b != 0 else None for a, b in zip(list1, list2)]
247
+ return res
QTube/utils/parsing.py CHANGED
@@ -3,7 +3,7 @@ import json
3
3
  import re
4
4
 
5
5
 
6
- def parse_arguments():
6
+ def parse_arguments() -> dict:
7
7
  """Parses command line arguments.
8
8
 
9
9
  Args:
@@ -217,6 +217,46 @@ def parse_arguments():
217
217
  help="Projection the videos need to be in. Default: None",
218
218
  )
219
219
 
220
+ parser.add_argument(
221
+ "-vt",
222
+ "--views_threshold",
223
+ metavar="",
224
+ type=int,
225
+ help="Minimum number of views. Default: 0",
226
+ )
227
+
228
+ parser.add_argument(
229
+ "-lt",
230
+ "--likes_threshold",
231
+ metavar="",
232
+ type=int,
233
+ help="Minimum number of likes. Default: 0",
234
+ )
235
+
236
+ parser.add_argument(
237
+ "-ct",
238
+ "--comments_threshold",
239
+ metavar="",
240
+ type=int,
241
+ help="Minimum number of comments. Default: 0",
242
+ )
243
+
244
+ parser.add_argument(
245
+ "-lvr",
246
+ "--likes_to_views_ratio",
247
+ metavar="",
248
+ type=float,
249
+ help="Minimum ratio of likes to views. Default: 0",
250
+ )
251
+
252
+ parser.add_argument(
253
+ "-cvr",
254
+ "--comments_to_views_ratio",
255
+ metavar="",
256
+ type=float,
257
+ help="Minimum ratio of comments to views. Default: 0",
258
+ )
259
+
220
260
  parser.add_argument(
221
261
  "-rf",
222
262
  "--run_frequency",
@@ -233,6 +273,20 @@ def parse_arguments():
233
273
  help="Determines whether to add shorts. Default: True",
234
274
  )
235
275
 
276
+ parser.add_argument(
277
+ "-app",
278
+ "--allow_paid_promotions",
279
+ action="store_false",
280
+ help="Allow videos containing paid advertising. Default: True",
281
+ )
282
+
283
+ parser.add_argument(
284
+ "-mfk",
285
+ "--only_made_for_kids",
286
+ action="store_true",
287
+ help="Determines whether to only add videos that are made for kids, based on Youtube and FTC guidelines. Default: False",
288
+ )
289
+
236
290
  parser.add_argument(
237
291
  "-kd",
238
292
  "--keep_duplicates",
@@ -268,7 +322,7 @@ def parse_arguments():
268
322
  return vars(parser.parse_args())
269
323
 
270
324
 
271
- def format_arguments(args_dict):
325
+ def format_arguments(args_dict) -> dict:
272
326
  """Formats the parsed command line arguments (written with the help of AI, regex is witchcraft to me).
273
327
 
274
328
  Args:
@@ -306,7 +360,7 @@ def format_arguments(args_dict):
306
360
  return args_dict
307
361
 
308
362
 
309
- def format_arguments_legacy(args_dict):
363
+ def format_arguments_legacy(args_dict) -> dict:
310
364
  """Formats the parsed command line arguments (legacy function).
311
365
 
312
366
  Args:
@@ -36,7 +36,7 @@ def get_subscriptions(youtube, next_page_token=None) -> dict:
36
36
  return channels
37
37
 
38
38
 
39
- def get_channel_info(youtube, handle: str):
39
+ def get_channel_info(youtube, handle: str) -> dict:
40
40
  """Retrieves basic information about a YT channel.
41
41
 
42
42
  Args:
@@ -49,9 +49,7 @@ def get_channel_info(youtube, handle: str):
49
49
  channel = {}
50
50
 
51
51
  response = (
52
- youtube.channels()
53
- .list(part="snippet", forHandle=handle)
54
- .execute(num_retries=5)
52
+ youtube.channels().list(part="snippet", forHandle=handle).execute(num_retries=5)
55
53
  )
56
54
 
57
55
  if "items" in response.keys():
@@ -66,7 +66,7 @@ def get_playlist_content(youtube, playlist_ID: str) -> list[str]:
66
66
  return videos_IDs
67
67
 
68
68
 
69
- def get_playlists_titles(youtube=None, playlist_IDs: list[str] = None):
69
+ def get_playlists_titles(youtube=None, playlist_IDs: list[str] = None) -> list[str]:
70
70
  """Retrieves the titles of a list of YT playlists.
71
71
 
72
72
  Args:
@@ -88,7 +88,9 @@ def get_playlists_titles(youtube=None, playlist_IDs: list[str] = None):
88
88
  return titles
89
89
 
90
90
 
91
- def get_playlists_video_counts(youtube=None, playlist_IDs: list[str] = None):
91
+ def get_playlists_video_counts(
92
+ youtube=None, playlist_IDs: list[str] = None
93
+ ) -> list[int]:
92
94
  """Retrieves the number of videos of a list of YT playlists.
93
95
 
94
96
  Args:
@@ -2,6 +2,8 @@ import isodate
2
2
 
3
3
  from pytube import YouTube
4
4
 
5
+ from QTube.utils import helpers, checks
6
+
5
7
 
6
8
  def make_video_requests(youtube, video_IDs: list[str]) -> dict:
7
9
  """Retrieves information on a list of YT videos.
@@ -16,7 +18,10 @@ def make_video_requests(youtube, video_IDs: list[str]) -> dict:
16
18
  video_IDs_str = ",".join(video_IDs)
17
19
  response = (
18
20
  youtube.videos()
19
- .list(part="snippet,contentDetails", id=video_IDs_str)
21
+ .list(
22
+ part="snippet,contentDetails,statistics,paidProductPlacementDetails,status",
23
+ id=video_IDs_str,
24
+ )
20
25
  .execute(num_retries=5)
21
26
  )
22
27
  return response
@@ -338,6 +343,158 @@ def get_projections(
338
343
  return projections
339
344
 
340
345
 
346
+ def get_view_counts(
347
+ youtube=None,
348
+ response: dict = None,
349
+ video_IDs: list[str] = None,
350
+ use_API: bool = False,
351
+ ) -> list[int]:
352
+ """Retrieves the number of views of a list of YT videos.
353
+
354
+ Args:
355
+ youtube (Resource): YT API resource.
356
+ response (dict[dict]): YT API response from the make_video_request function.
357
+ video_IDs (list[str]): List of video IDs.
358
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
359
+
360
+ Returns:
361
+ views (list[int]): List of YT videos views.
362
+ """
363
+ if use_API:
364
+ video_IDs_str = ",".join(video_IDs)
365
+ response = (
366
+ youtube.videos()
367
+ .list(part="statistics", id=video_IDs_str)
368
+ .execute(num_retries=5)
369
+ )
370
+
371
+ views = [int(vid["statistics"]["viewCount"]) for vid in response["items"]]
372
+
373
+ return views
374
+
375
+
376
+ def get_like_counts(
377
+ youtube=None,
378
+ response: dict = None,
379
+ video_IDs: list[str] = None,
380
+ use_API: bool = False,
381
+ ) -> list[int]:
382
+ """Retrieves the number of likes of a list of YT videos.
383
+
384
+ Args:
385
+ youtube (Resource): YT API resource.
386
+ response (dict[dict]): YT API response from the make_video_request function.
387
+ video_IDs (list[str]): List of video IDs.
388
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
389
+
390
+ Returns:
391
+ likes (list[int]): List of YT videos likes.
392
+ """
393
+ if use_API:
394
+ video_IDs_str = ",".join(video_IDs)
395
+ response = (
396
+ youtube.videos()
397
+ .list(part="statistics", id=video_IDs_str)
398
+ .execute(num_retries=5)
399
+ )
400
+
401
+ likes = [int(vid["statistics"]["likeCount"]) for vid in response["items"]]
402
+
403
+ return likes
404
+
405
+
406
+ def get_comment_counts(
407
+ youtube=None,
408
+ response: dict = None,
409
+ video_IDs: list[str] = None,
410
+ use_API: bool = False,
411
+ ) -> list[int]:
412
+ """Retrieves the number of comments of a list of YT videos.
413
+
414
+ Args:
415
+ youtube (Resource): YT API resource.
416
+ response (dict[dict]): YT API response from the make_video_request function.
417
+ video_IDs (list[str]): List of video IDs.
418
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
419
+
420
+ Returns:
421
+ comment_counts (list[int]): List of YT videos comment counts.
422
+ """
423
+ if use_API:
424
+ video_IDs_str = ",".join(video_IDs)
425
+ response = (
426
+ youtube.videos()
427
+ .list(part="statistics", id=video_IDs_str)
428
+ .execute(num_retries=5)
429
+ )
430
+
431
+ comment_counts = [
432
+ int(vid["statistics"]["commentCount"]) for vid in response["items"]
433
+ ]
434
+
435
+ return comment_counts
436
+
437
+
438
+ def get_likes_to_views_ratio(
439
+ likes,
440
+ views,
441
+ youtube=None,
442
+ response: dict = None,
443
+ video_IDs: list[str] = None,
444
+ use_API: bool = False,
445
+ ) -> list[int | float]:
446
+ """Retrieves the likes to views ratio of a list of YT videos.
447
+
448
+ Args:
449
+ likes (list[int]): List of the number of likes.
450
+ views (list[int]): List of the number of views.
451
+ youtube (Resource): YT API resource.
452
+ response (dict[dict]): YT API response from the make_video_request function.
453
+ video_IDs (list[str]): List of video IDs.
454
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
455
+
456
+ Returns:
457
+ ratio (list[int|float]): List of YT videos' likes to views ratios.
458
+ """
459
+ if use_API:
460
+ views = get_view_counts(youtube, response, video_IDs, use_API)
461
+ likes = get_like_counts(youtube, response, video_IDs, use_API)
462
+
463
+ ratio = helpers.divide_lists(likes, views, False)
464
+
465
+ return ratio
466
+
467
+
468
+ def get_comments_to_views_ratio(
469
+ likes,
470
+ views,
471
+ youtube=None,
472
+ response: dict = None,
473
+ video_IDs: list[str] = None,
474
+ use_API: bool = False,
475
+ ) -> list[int | float]:
476
+ """Retrieves the comments to views ratio of a list of YT videos.
477
+
478
+ Args:
479
+ comments (list[int]): List of the number of comments.
480
+ views (list[int]): List of the number of views.
481
+ youtube (Resource): YT API resource.
482
+ response (dict[dict]): YT API response from the make_video_request function.
483
+ video_IDs (list[str]): List of video IDs.
484
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
485
+
486
+ Returns:
487
+ views (list[int|float]): List of YT videos' comments to views ratios.
488
+ """
489
+ if use_API:
490
+ views = get_view_counts(youtube, response, video_IDs, use_API)
491
+ comments = get_comment_counts(youtube, response, video_IDs, use_API)
492
+
493
+ ratio = helpers.divide_lists(comments, views, False)
494
+
495
+ return ratio
496
+
497
+
341
498
  def has_captions(
342
499
  youtube=None,
343
500
  response: dict = None,
@@ -372,8 +529,8 @@ def is_short(
372
529
  response: dict = None,
373
530
  video_IDs: list[str] = None,
374
531
  use_API: bool = False,
375
- ) -> list[str]:
376
- """Determines if videos are a short or not by putting a threshold on video duration.
532
+ ) -> list[bool]:
533
+ """Determines if videos are a short or not by putting a threshold on video duration and checking for a redirection at the youtube.com/shorts/*vid_ID* URL.
377
534
 
378
535
  Args:
379
536
  youtube (Resource): YT API resource.
@@ -382,13 +539,21 @@ def is_short(
382
539
  use_API (bool): Determines if a new API request is made or if the response dictionary is used.
383
540
 
384
541
  Returns:
385
- is_short (list[bool]): True if the video is shorter than 65 seconds, False otherwise.
542
+ is_a_short (list[bool]): True if the video is shorter than 181 seconds and there is no URL redirection, False otherwise.
386
543
  """
387
544
  durations = get_durations(youtube, response, video_IDs, use_API=use_API)
388
545
 
389
- is_short = [True if length <= 65.0 else False for length in durations]
546
+ is_short_vid = [
547
+ True if length <= 181 else False for length in durations
548
+ ] # Shorts cannot last over 3 minutes.
390
549
 
391
- return is_short
550
+ is_not_redirected = [
551
+ not checks.check_URL_redirect("https://www.youtube.com/shorts/" + vid_ID, 303)
552
+ for vid_ID in video_IDs
553
+ ] # Shorts do not trigger a redirection.
554
+
555
+ is_a_short = is_short_vid and is_not_redirected
556
+ return is_a_short
392
557
 
393
558
 
394
559
  def is_live(
@@ -396,7 +561,7 @@ def is_live(
396
561
  response: dict = None,
397
562
  video_IDs: list[str] = None,
398
563
  use_API: bool = False,
399
- ):
564
+ ) -> list[str]:
400
565
  """Retrieves the live status of YT videos.
401
566
 
402
567
  Args:
@@ -421,3 +586,64 @@ def is_live(
421
586
  ]
422
587
 
423
588
  return live_statuses
589
+
590
+
591
+ def has_paid_advertising(
592
+ youtube=None,
593
+ response: dict = None,
594
+ video_IDs: list[str] = None,
595
+ use_API: bool = False,
596
+ )-> list[bool]:
597
+ """Determines if a video contains paid advertising.
598
+
599
+ Args:
600
+ youtube (Resource): YT API resource.
601
+ response (dict[dict]): YT API response from the make_video_request function.
602
+ video_IDs (list[str]): List of video IDs.
603
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
604
+
605
+ Returns:
606
+ (list[bool]): True if the video contains paid advertising, False otherwise.
607
+ """
608
+
609
+ if use_API:
610
+ video_IDs_str = ",".join(video_IDs)
611
+ response = (
612
+ youtube.videos()
613
+ .list(part="paidProductPlacementDetails", id=video_IDs_str)
614
+ .execute(num_retries=5)
615
+ )
616
+
617
+ return [
618
+ vid["paidProductPlacementDetails"]["hasPaidProductPlacement"]
619
+ for vid in response["items"]
620
+ ]
621
+
622
+
623
+ def is_made_for_kids(
624
+ youtube=None,
625
+ response: dict = None,
626
+ video_IDs: list[str] = None,
627
+ use_API: bool = False,
628
+ )-> list[bool]:
629
+ """Determines if a video is appropriate for children (based on YT's guidelines).
630
+
631
+ Args:
632
+ youtube (Resource): YT API resource.
633
+ response (dict[dict]): YT API response from the make_video_request function.
634
+ video_IDs (list[str]): List of video IDs.
635
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
636
+
637
+ Returns:
638
+ (list(bool)): True if the video is made for kids, False otherwise.
639
+ """
640
+
641
+ if use_API:
642
+ video_IDs_str = ",".join(video_IDs)
643
+ response = (
644
+ youtube.videos()
645
+ .list(part="status", id=video_IDs_str)
646
+ .execute(num_retries=5)
647
+ )
648
+
649
+ return [vid["status"]["madeForKids"] for vid in response["items"]]
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) [2024] [Killian Lebreton]
3
+ Copyright (c) [2025] [Killian Lebreton]
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: QTube
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: Automatically add Youtube videos to a playlist.
5
5
  Home-page: https://github.com/Killian42/QTube
6
6
  Author: Killian Lebreton
@@ -8,7 +8,7 @@ Author-email: Killian Lebreton <killian.lebreton35@gmail.com>
8
8
  Maintainer-email: Killian Lebreton <killian.lebreton35@gmail.com>
9
9
  License: MIT License
10
10
 
11
- Copyright (c) [2024] [Killian Lebreton]
11
+ Copyright (c) [2025] [Killian Lebreton]
12
12
 
13
13
  Permission is hereby granted, free of charge, to any person obtaining a copy
14
14
  of this software and associated documentation files (the "Software"), to deal
@@ -98,11 +98,14 @@ Each of these rules is based on putting some kind of constraint on video propert
98
98
  * Language filtering
99
99
  * Caption filtering
100
100
  * Duration filtering
101
+ * Views, likes & comments counts filtering
101
102
  * Livestream filtering
102
103
  * Premiere filtering
103
104
  * Quality filtering
104
105
  * Upload date filtering
105
106
  * Shorts filtering
107
+ * Paid promotion filtering
108
+ * Made for Kids filtering
106
109
  * Duplicate checking
107
110
 
108
111
  ## How to use
@@ -145,8 +148,15 @@ For more versatile uses, you can also use command line arguments with the [qtube
145
148
  |`lowest_framerate`|Yes|Minimum framerate. Videos with framerates stricly lower than this value will not be added.|Positive integer|
146
149
  |`preferred_dimensions`|Yes|Dimension the videos need to be in.|*2D*, *3D* or both|
147
150
  |`preferred_projections`|Yes|Projection the videos need to be in.|*rectangular*, *360* or both|
151
+ |`views_threshold`|No|Minimum number of times videos have been viewed.|Positive integer|
152
+ |`likes_threshold`|No|Minimum number of times videos have been liked.|Positive integer|
153
+ |`comments_threshold`|No|Minimum number of times videos have been commented on.|Positive integer|
154
+ |`likes_to_views_ratio`|No|Minimum likes to views ratio.|Positive float between 0 & 1|
155
+ |`comments_to_views_ratio`|No|Minimum comments to views ratio.|Positive float between 0 & 1|
148
156
  |`run_frequency`|No|Defines the duration, in days, of the timeframe considered by the software. Can be interpreted as the frequency the program should be run.|*daily*, *weekly*, *monthly* or any positive integer|
149
157
  |`keep_shorts`|No|Determines whether to add shorts.|boolean|
158
+ |`allow_paid_promotions`|No|Determines whether to add videos containing paid advertisement.|boolean|
159
+ |`only_made_for_kids`|No|Determines whether to only add videos that are *Made for Kids* (based on [Youtube and FTC guidelines](https://support.google.com/youtube/answer/9528076)).|boolean|
150
160
  |`keep_duplicates`|No|Determines whether to add videos that are already in the playlist.|boolean|
151
161
  |`upload_playlist_ID`|No|ID of the playlist the videos will be added to. Playlist IDs are found at the end of their URL: `https://www.youtube.com/playlist?list=*playlist_ID*`|Playlist ID|
152
162
  |`override_json`|No|Allow command line arguments to override user_params.json parameters.|boolean|
@@ -193,8 +203,15 @@ The following *user_params.json* file would add every new videos from channels y
193
203
  "lowest_framerate": null,
194
204
  "preferred_dimensions": null,
195
205
  "preferred_projections": null,
206
+ "views_threshold": 0,
207
+ "likes_threshold": 0,
208
+ "comments_threshold": 0,
209
+ "likes_to_views_ratio": 0,
210
+ "comments_to_views_ratio": 0,
196
211
  "run_frequency":"daily",
197
212
  "keep_shorts": true,
213
+ "allow_paid_promotions": true,
214
+ "only_made_for_kids": false,
198
215
  "keep_duplicates": false,
199
216
  "upload_playlist_ID": "your_playlist_ID",
200
217
  "override_json":false,
@@ -230,8 +247,15 @@ The following *user_params.json* file would only add videos with good quality.
230
247
  "lowest_framerate": 60,
231
248
  "preferred_dimensions": ["2D"],
232
249
  "preferred_projections": ["rectangular"],
250
+ "views_threshold": 0,
251
+ "likes_threshold": 0,
252
+ "comments_threshold": 0,
253
+ "likes_to_views_ratio": 0,
254
+ "comments_to_views_ratio": 0,
233
255
  "run_frequency":"daily",
234
256
  "keep_shorts": true,
257
+ "allow_paid_promotions": true,
258
+ "only_made_for_kids": false,
235
259
  "keep_duplicates": false,
236
260
  "upload_playlist_ID": "your_playlist_ID",
237
261
  "override_json":false,
@@ -267,8 +291,15 @@ The following *user_params.json* file would only add the *$1 vs.* MrBeast videos
267
291
  "lowest_framerate": null,
268
292
  "preferred_dimensions": ["2D"],
269
293
  "preferred_projections": ["rectangular"],
294
+ "views_threshold": 0,
295
+ "likes_threshold": 0,
296
+ "comments_threshold": 0,
297
+ "likes_to_views_ratio": 0,
298
+ "comments_to_views_ratio": 0,
270
299
  "run_frequency":"daily",
271
300
  "keep_shorts": false,
301
+ "allow_paid_promotions": true,
302
+ "only_made_for_kids": false,
272
303
  "keep_duplicates": false,
273
304
  "upload_playlist_ID": "your_playlist_ID",
274
305
  "override_json":false,
@@ -0,0 +1,17 @@
1
+ QTube/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ QTube/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ QTube/scripts/qtube.py,sha256=7sr4UnaNtxjUAK_cxW9_fgLzW3jMD-kCkBKJXmXh0_o,35108
4
+ QTube/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ QTube/utils/checks.py,sha256=EV-oV89Ufd02fAzsT68wyVCziSMlnbo1NR1XiNqGa0k,13159
6
+ QTube/utils/helpers.py,sha256=fl1_fePwy7ot-KRFB9Ieq7fMFtD8Q4TriK8cW17qPy0,9187
7
+ QTube/utils/parsing.py,sha256=LQ7onVUyqef9bQOS54YNixOHdJGkfydsazP7ijQQoNA,10789
8
+ QTube/utils/youtube/captions.py,sha256=0jUs8SH4L4d2RTS4QHJ5J2Zd9qe3SOHf9VZW966NuY8,1591
9
+ QTube/utils/youtube/channels.py,sha256=_IITSU3tZJLqrJkdQOLGAwTyrrsZb-WywRq2LyqXVBQ,3331
10
+ QTube/utils/youtube/playlists.py,sha256=kMWeaxGp09J3IY6UWpB7qKY7peTpIBlJkMUDb7CDIpM,3874
11
+ QTube/utils/youtube/videos.py,sha256=YWhpLQVADr95lp1XMgnkg5z05YOxtBgpEauUz95zMeQ,20607
12
+ QTube-2.4.0.dist-info/LICENSE.txt,sha256=cIZNbD-BZYZPzWYHhtE-iUCasUxQIwWzALL9nZh32pQ,1098
13
+ QTube-2.4.0.dist-info/METADATA,sha256=SyyF5njBo11yORDL9_UBmRSGlni982O2ZNgyXiP-G28,17798
14
+ QTube-2.4.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
15
+ QTube-2.4.0.dist-info/entry_points.txt,sha256=Q3SLDyRahuzrNnHC3UOkMH5gM_Um3eCLiLG4vSTPjqE,51
16
+ QTube-2.4.0.dist-info/top_level.txt,sha256=z6oXrT8BiTTZuygjsbfe-NE4rmkHlDK8euAL9m6BT4A,6
17
+ QTube-2.4.0.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- QTube/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- QTube/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- QTube/scripts/qtube.py,sha256=4ZCSNwWJ90ysTOu92VZI2SUXYsmDa9aXGhA_z8s-Cf0,31002
4
- QTube/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- QTube/utils/checks.py,sha256=g8KLAaOP9cregzfJLou_PkODFtdQNECFOoSoTnECXn8,11819
6
- QTube/utils/helpers.py,sha256=F2393sh4s3L8Ax8Gljogm3o1o_xzzJe_7MiwxiSWAXo,8433
7
- QTube/utils/parsing.py,sha256=SIS8wmhv_bQWf8QwH_GGr9yvTOPuh6tTJUjhy-XAiFU,9445
8
- QTube/utils/youtube/captions.py,sha256=0jUs8SH4L4d2RTS4QHJ5J2Zd9qe3SOHf9VZW966NuY8,1591
9
- QTube/utils/youtube/channels.py,sha256=8K-5_Ff8zHyKhEM02cdwMEV6HLU14MngxI1v2yJh6iI,3343
10
- QTube/utils/youtube/playlists.py,sha256=Cdohhb2M4bxKiTnZ-R-0xNXPV050nhhI6hepvtDVpIM,3840
11
- QTube/utils/youtube/videos.py,sha256=FFQVjHktk1vRAqfL6BE1zGsBmgomMzn49R4wKH_5oVc,13282
12
- QTube-2.2.0.dist-info/LICENSE.txt,sha256=cctyZoZUseENxkeq5p_C5oCFYpK0-ECku_sCZmWbL0E,1098
13
- QTube-2.2.0.dist-info/METADATA,sha256=jcN88dOhmghemBpXB_T5oe_8WywvGe4BqmESRTo_U9I,16368
14
- QTube-2.2.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
15
- QTube-2.2.0.dist-info/entry_points.txt,sha256=Q3SLDyRahuzrNnHC3UOkMH5gM_Um3eCLiLG4vSTPjqE,51
16
- QTube-2.2.0.dist-info/top_level.txt,sha256=z6oXrT8BiTTZuygjsbfe-NE4rmkHlDK8euAL9m6BT4A,6
17
- QTube-2.2.0.dist-info/RECORD,,
File without changes