QTube 2.1.1__py3-none-any.whl → 2.3.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
@@ -23,31 +23,6 @@ import QTube.utils.youtube.videos
23
23
 
24
24
  def main():
25
25
  """Checks Youtube for new videos and add a selection of these videos to a playlist, based on user defined parameters."""
26
- ### Software version checking
27
- version, latest_release = QTube.utils.checks.check_version()
28
- latest_url = "https://github.com/Killian42/QTube/releases/latest"
29
-
30
- if latest_release is None:
31
- print("Failed to check the latest release version:\n")
32
- else:
33
- comp = QTube.utils.checks.compare_software_versions(version, latest_release)
34
- if comp == "same":
35
- print(
36
- f"The latest stable version of the software, v{version}, is currently runnning.\n"
37
- )
38
- elif comp == "older":
39
- print(
40
- f"You are currently running version v{version}.\nConsider upgrading to the latest stable release (v{latest_release}) at {latest_url}.\n"
41
- )
42
- elif comp == "newer":
43
- print(
44
- f"You are currently running version {version}.\nThis version is not a stable release. Consider installing the latest stable release ({latest_release}) at {latest_url}.\n"
45
- )
46
- elif comp == "pre-release":
47
- print(
48
- f"You are currently running version v{version}.\nThis is a pre-release version. Consider installing the latest stable release (v{latest_release}) at {latest_url}.\n"
49
- )
50
-
51
26
  ### User parameters loading
52
27
  ## JSON parameters file opening
53
28
  try:
@@ -74,14 +49,90 @@ def main():
74
49
 
75
50
  ## Parameters checking
76
51
  if QTube.utils.checks.check_user_params(user_params_dict) is not True:
77
- print("User defined parameters are not correct. Check the template and retry.")
52
+ print(
53
+ "User defined parameters are not correctly formatted. Check the template and retry."
54
+ )
78
55
  sys.exit()
79
- else:
80
- print("The user defined parameters are correctly formatted.\n")
81
56
 
82
- ## Verbosity options
57
+ ## Verbosity and fancy text options loading
58
+ fancy = user_params_dict["fancy_mode"]
83
59
  verb = user_params_dict["verbosity"]
84
- print(f"The following verbosity options are enabled: {verb}.\n")
60
+
61
+ ### Software version checking
62
+ version, latest_release = QTube.utils.checks.check_version()
63
+ latest_url = "https://github.com/Killian42/QTube/releases/latest"
64
+
65
+ QTube.utils.helpers.print2(
66
+ f"QTube v{version}\n", fancy, "info", ["internal"], ["internal"]
67
+ )
68
+
69
+ if latest_release is None:
70
+ QTube.utils.helpers.print2(
71
+ "Failed to check the latest release version:\n",
72
+ fancy,
73
+ "fail",
74
+ ["internal"],
75
+ ["internal"],
76
+ )
77
+ else:
78
+ comp = QTube.utils.checks.compare_software_versions(version, latest_release)
79
+ if comp == "same":
80
+ QTube.utils.helpers.print2(
81
+ f"The latest stable version of the software, v{version}, is currently runnning.\n",
82
+ fancy,
83
+ "success",
84
+ ["internal"],
85
+ ["internal"],
86
+ )
87
+ elif comp == "older":
88
+ QTube.utils.helpers.print2(
89
+ f"You are currently running version v{version}.\nConsider upgrading to the latest stable release (v{latest_release}) at {latest_url}.\n",
90
+ fancy,
91
+ "warning",
92
+ ["internal"],
93
+ ["internal"],
94
+ )
95
+ elif comp == "newer":
96
+ QTube.utils.helpers.print2(
97
+ f"You are currently running version {version}.\nThis version is not a stable release. Consider installing the latest stable release ({latest_release}) at {latest_url}.\n",
98
+ fancy,
99
+ "warning",
100
+ ["internal"],
101
+ ["internal"],
102
+ )
103
+ elif comp == "pre-release":
104
+ QTube.utils.helpers.print2(
105
+ f"You are currently running version v{version}.\nThis is a pre-release version. Consider installing the latest stable release (v{latest_release}) at {latest_url}.\n",
106
+ fancy,
107
+ "warning",
108
+ ["internal"],
109
+ ["internal"],
110
+ )
111
+
112
+ ## Verbosity and fancy text options displaying
113
+ if fancy:
114
+ QTube.utils.helpers.print2(
115
+ f"The fancy text option is enabled.",
116
+ fancy,
117
+ "info",
118
+ ["internal"],
119
+ ["internal"],
120
+ )
121
+ else:
122
+ QTube.utils.helpers.print2(
123
+ f"The fancy text option is disabled.",
124
+ fancy,
125
+ "info",
126
+ ["internal"],
127
+ ["internal"],
128
+ )
129
+ QTube.utils.helpers.print2(
130
+ f"The following verbosity options are enabled: {verb}.\n",
131
+ fancy,
132
+ "info",
133
+ ["internal"],
134
+ ["internal"],
135
+ )
85
136
 
86
137
  ### Youtube API login
87
138
  credentials = None
@@ -89,30 +140,46 @@ def main():
89
140
  ## token.pickle stores the user's credentials from previously successful logins
90
141
  if os.path.exists("token.pickle"):
91
142
  QTube.utils.helpers.print2(
92
- "Loading credentials from pickle file...", ["all", "credentials"], verb
143
+ "Loading credentials from pickle file...",
144
+ fancy,
145
+ "info",
146
+ ["all", "credentials"],
147
+ verb,
93
148
  )
94
149
 
95
150
  with open("token.pickle", "rb") as token:
96
151
  credentials = pickle.load(token)
97
152
 
98
153
  QTube.utils.helpers.print2(
99
- "Credentials loaded from pickle file", ["all", "credentials"], verb
154
+ "Credentials loaded from pickle file",
155
+ fancy,
156
+ "success",
157
+ ["all", "credentials"],
158
+ verb,
100
159
  )
101
160
 
102
161
  ## If there are no valid credentials available, then either refresh the token or log in.
103
162
  if not credentials or not credentials.valid:
104
163
  if credentials and credentials.expired and credentials.refresh_token:
105
164
  QTube.utils.helpers.print2(
106
- "Refreshing access token...", ["all", "credentials"], verb
165
+ "Refreshing access token...",
166
+ fancy,
167
+ "info",
168
+ ["all", "credentials"],
169
+ verb,
107
170
  )
108
171
 
109
172
  credentials.refresh(Request())
110
173
  QTube.utils.helpers.print2(
111
- "Access token refreshed\n", ["all", "credentials"], verb
174
+ "Access token refreshed\n",
175
+ fancy,
176
+ "success",
177
+ ["all", "credentials"],
178
+ verb,
112
179
  )
113
180
  else:
114
181
  QTube.utils.helpers.print2(
115
- "Fetching New Tokens...", ["all", "credentials"], verb
182
+ "Fetching New Tokens...", fancy, "info", ["all", "credentials"], verb
116
183
  )
117
184
  flow = InstalledAppFlow.from_client_secrets_file(
118
185
  "client_secrets.json",
@@ -129,18 +196,26 @@ def main():
129
196
  credentials = flow.credentials
130
197
 
131
198
  QTube.utils.helpers.print2(
132
- "New token fetched\n", ["all", "credentials"], verb
199
+ "New token fetched\n", fancy, "success", ["all", "credentials"], verb
133
200
  )
134
201
 
135
202
  # Save the credentials for the next run
136
203
  with open("token.pickle", "wb") as f:
137
204
  QTube.utils.helpers.print2(
138
- "Saving Credentials for Future Use...", ["all", "credentials"], verb
205
+ "Saving Credentials for Future Use...",
206
+ fancy,
207
+ "info",
208
+ ["all", "credentials"],
209
+ verb,
139
210
  )
140
211
 
141
212
  pickle.dump(credentials, f)
142
213
  QTube.utils.helpers.print2(
143
- "Credentials saved\n", ["all", "credentials"], verb
214
+ "Credentials saved\n",
215
+ fancy,
216
+ "success",
217
+ ["all", "credentials"],
218
+ verb,
144
219
  )
145
220
 
146
221
  ### Building API resource
@@ -151,16 +226,21 @@ def main():
151
226
  ## Checking the playlist ID
152
227
  playlist_ID = user_params_dict["upload_playlist_ID"]
153
228
  user_info = QTube.utils.helpers.handle_http_errors(
154
- verb, QTube.utils.youtube.channels.get_user_info, youtube
229
+ verb, fancy, QTube.utils.youtube.channels.get_user_info, youtube
155
230
  )
156
231
  if not QTube.utils.helpers.handle_http_errors(
157
- verb, QTube.utils.checks.check_playlist_id, youtube, user_info, playlist_ID
232
+ verb,
233
+ fancy,
234
+ QTube.utils.checks.check_playlist_id,
235
+ youtube,
236
+ user_info,
237
+ playlist_ID,
158
238
  ):
159
239
  sys.exit()
160
240
 
161
241
  ## Dictionnary of subscribed channels names and IDs
162
242
  subbed_channels_info = QTube.utils.helpers.handle_http_errors(
163
- verb, QTube.utils.youtube.channels.get_subscriptions, youtube
243
+ verb, fancy, QTube.utils.youtube.channels.get_subscriptions, youtube
164
244
  )
165
245
 
166
246
  ## Dictionnary of extra channels names and IDs
@@ -219,6 +299,7 @@ def main():
219
299
  for sub_dict in split_channels:
220
300
  partial = QTube.utils.helpers.handle_http_errors(
221
301
  verb,
302
+ fancy,
222
303
  QTube.utils.youtube.channels.get_uploads_playlists,
223
304
  youtube,
224
305
  list(sub_dict.values()),
@@ -230,12 +311,20 @@ def main():
230
311
  recent_videos = {}
231
312
  for ch_name, playlist_Id in wanted_channels_upload_playlists.items():
232
313
  latest_partial = QTube.utils.helpers.handle_http_errors(
233
- verb, QTube.utils.youtube.playlists.get_recent_videos, youtube, playlist_Id
314
+ verb,
315
+ fancy,
316
+ QTube.utils.youtube.playlists.get_recent_videos,
317
+ youtube,
318
+ playlist_Id,
234
319
  )
235
320
 
236
321
  if latest_partial == "ignore":
237
322
  QTube.utils.helpers.print2(
238
- f"Channel {ch_name} has no public videos.", ["all", "func"], verb
323
+ f"Channel {ch_name} has no public videos.",
324
+ fancy,
325
+ "warning",
326
+ ["all", "func"],
327
+ verb,
239
328
  )
240
329
  continue
241
330
 
@@ -282,6 +371,7 @@ def main():
282
371
  for sub_dict in split_videos:
283
372
  partial = QTube.utils.helpers.handle_http_errors(
284
373
  verb,
374
+ fancy,
285
375
  QTube.utils.youtube.videos.make_video_requests,
286
376
  youtube,
287
377
  sub_dict.keys(),
@@ -320,6 +410,25 @@ def main():
320
410
  # Live status retrieving
321
411
  live_statuses = QTube.utils.youtube.videos.is_live(response=responses)
322
412
 
413
+ # View counts retrieving
414
+ view_counts = QTube.utils.youtube.videos.get_view_counts(response=responses)
415
+
416
+ # Like counts retrieving
417
+ like_counts = QTube.utils.youtube.videos.get_like_counts(response=responses)
418
+
419
+ # Comment counts retrieving
420
+ comment_counts = QTube.utils.youtube.videos.get_comment_counts(response=responses)
421
+
422
+ # Likes/views ratio retrieving
423
+ likes_to_views_ratio = QTube.utils.youtube.videos.get_likes_to_views_ratio(
424
+ like_counts, view_counts
425
+ )
426
+
427
+ # Comments/views ratio retrieving
428
+ comments_to_views_ratio = QTube.utils.youtube.videos.get_likes_to_views_ratio(
429
+ comment_counts, view_counts
430
+ )
431
+
323
432
  # Resolutions retrieving (does not use YT API)
324
433
  lowest_resolution = user_params_dict.get("lowest_resolution")
325
434
  if lowest_resolution is not None:
@@ -339,6 +448,7 @@ def main():
339
448
  if need_captions:
340
449
  captions_responses = QTube.utils.helpers.handle_http_errors(
341
450
  verb,
451
+ fancy,
342
452
  QTube.utils.youtube.captions.make_caption_requests,
343
453
  youtube,
344
454
  videos.keys(),
@@ -378,6 +488,21 @@ def main():
378
488
  # Live statuses
379
489
  vid_info.update({"live status": live_statuses[index]})
380
490
 
491
+ # Views
492
+ vid_info.update({"views": view_counts[index]})
493
+
494
+ # Likes
495
+ vid_info.update({"likes": like_counts[index]})
496
+
497
+ # Comments
498
+ vid_info.update({"comments": comment_counts[index]})
499
+
500
+ # Likes/views
501
+ vid_info.update({"likes_to_views_ratio": likes_to_views_ratio[index]})
502
+
503
+ # Comments/views
504
+ vid_info.update({"comments_to_views_ratio": comments_to_views_ratio[index]})
505
+
381
506
  # Resolutions
382
507
  if lowest_resolution is not None:
383
508
  vid_info.update({"resolutions": resolutions[index]})
@@ -452,6 +577,12 @@ def main():
452
577
  lowest_definition = user_params_dict.get("lowest_definition")
453
578
  preferred_dimensions = user_params_dict.get("preferred_dimensions")
454
579
 
580
+ views_threshold = user_params_dict.get("views_threshold")
581
+ likes_threshold = user_params_dict.get("likes_threshold")
582
+ comments_threshold = user_params_dict.get("comments_threshold")
583
+ likes_to_views_ratio_threshold = user_params_dict.get("likes_to_views_ratio")
584
+ comments_to_views_ratio_threshold = user_params_dict.get("comments_to_views_ratio")
585
+
455
586
  # Duration filtering
456
587
  if min_max_durations is not None:
457
588
  for vid_info in videos.values():
@@ -680,6 +811,43 @@ def main():
680
811
  ):
681
812
  vid_info.update({"to add": False})
682
813
 
814
+ # Views filtering
815
+ if views_threshold > 0:
816
+ for vid_ID, vid_info in videos.items():
817
+ if vid_info["to add"] and vid_info["views"] < views_threshold:
818
+ vid_info.update({"to add": False})
819
+
820
+ # Likes Filtering
821
+ if likes_threshold > 0:
822
+ for vid_ID, vid_info in videos.items():
823
+ if vid_info["to add"] and vid_info["likes"] < likes_threshold:
824
+ vid_info.update({"to add": False})
825
+
826
+ # Comments filtering
827
+ if comments_threshold > 0:
828
+ for vid_ID, vid_info in videos.items():
829
+ if vid_info["to add"] and vid_info["comments"] < comments_threshold:
830
+ vid_info.update({"to add": False})
831
+
832
+ # Likes/views ratio filtering
833
+ if likes_to_views_ratio_threshold > 0:
834
+ for vid_ID, vid_info in videos.items():
835
+ if (
836
+ vid_info["to add"]
837
+ and vid_info["likes_to_views_ratio"] < likes_to_views_ratio_threshold
838
+ ):
839
+ vid_info.update({"to add": False})
840
+
841
+ # Comments/views ratio filtering
842
+ if comments_to_views_ratio_threshold > 0:
843
+ for vid_ID, vid_info in videos.items():
844
+ if (
845
+ vid_info["to add"]
846
+ and vid_info["comments_to_views_ratio"]
847
+ < comments_to_views_ratio_threshold
848
+ ):
849
+ vid_info.update({"to add": False})
850
+
683
851
  ## Selecting correct videos
684
852
  videos_to_add = {
685
853
  vid_ID: vid_info for vid_ID, vid_info in videos.items() if vid_info["to add"]
@@ -689,29 +857,66 @@ def main():
689
857
  playlist_title = QTube.utils.youtube.playlists.get_playlists_titles(
690
858
  youtube, [playlist_ID]
691
859
  )[0]
860
+ playlist_video_count = QTube.utils.youtube.playlists.get_playlists_video_counts(
861
+ youtube, [playlist_ID]
862
+ )[0]
863
+
692
864
  if len(videos_to_add) != 0: # Checks if there are actually videos to add
693
- QTube.utils.helpers.print2(
694
- f"The following videos will be added to the {playlist_title} playlist:",
695
- ["all", "videos"],
696
- verb,
697
- )
698
- for vid_ID, vid_info in videos_to_add.items():
699
- QTube.utils.helpers.handle_http_errors(
865
+ if (
866
+ playlist_video_count + len(videos_to_add) > 5000
867
+ ): # Checks if current video count + new videos would exceed 5k (YT playlist size limit)
868
+ QTube.utils.helpers.print2(
869
+ f"The {playlist_title} playlist would reach or exceed the 5000 size limit if the following videos were added to it:",
870
+ fancy,
871
+ "fail",
872
+ ["all", "videos"],
700
873
  verb,
701
- QTube.utils.youtube.playlists.add_to_playlist,
702
- youtube,
703
- playlist_ID,
704
- vid_ID,
705
874
  )
706
-
875
+ for vid_ID, vid_info in videos_to_add.items():
876
+ QTube.utils.helpers.print2(
877
+ f"From {vid_info['channel name']}, the video named: {vid_info['original title']} would have been added.\n It is available at: https://www.youtube.com/watch?v={vid_ID}",
878
+ fancy,
879
+ "video",
880
+ ["all", "videos"],
881
+ verb,
882
+ )
707
883
  QTube.utils.helpers.print2(
708
- f"From {vid_info['channel name']}, the video named: {vid_info['original title']} has been added.",
884
+ f"Remove at least {len(videos_to_add)} videos from the {playlist_title} playlist so that the new one(s) can be added.",
885
+ fancy,
886
+ "warning",
709
887
  ["all", "videos"],
710
888
  verb,
711
889
  )
890
+ else:
891
+ QTube.utils.helpers.print2(
892
+ f"The following videos will be added to the {playlist_title} playlist:",
893
+ fancy,
894
+ "info",
895
+ ["all", "videos"],
896
+ verb,
897
+ )
898
+ for vid_ID, vid_info in videos_to_add.items():
899
+ QTube.utils.helpers.handle_http_errors(
900
+ verb,
901
+ fancy,
902
+ QTube.utils.youtube.playlists.add_to_playlist,
903
+ youtube,
904
+ playlist_ID,
905
+ vid_ID,
906
+ )
907
+
908
+ QTube.utils.helpers.print2(
909
+ f"From {vid_info['channel name']}, the video named: {vid_info['original title']} has been added.",
910
+ fancy,
911
+ "video",
912
+ ["all", "videos"],
913
+ verb,
914
+ )
712
915
  else:
713
916
  QTube.utils.helpers.print2(
714
917
  f"No new videos to add to the {playlist_title} playlist.",
918
+ fancy,
919
+ "info",
715
920
  ["all", "videos"],
716
921
  verb,
717
922
  )
QTube/utils/checks.py CHANGED
@@ -332,6 +332,23 @@ def check_user_params(params_dict: dict) -> bool:
332
332
  isinstance(params_dict.get("ignore_livestreams"), bool),
333
333
  # Premieres
334
334
  isinstance(params_dict.get("ignore_premieres"), bool),
335
+ # Fancy text
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,
335
352
  ]
336
353
 
337
354
  ok = all(checks)
@@ -352,7 +369,11 @@ def check_playlist_id(youtube, user_info: dict, test_playlist_ID: str) -> bool:
352
369
  """
353
370
  user_channel_ID = user_info["items"][0]["id"]
354
371
 
355
- response = youtube.playlists().list(part="snippet", id=test_playlist_ID).execute()
372
+ response = (
373
+ youtube.playlists()
374
+ .list(part="snippet", id=test_playlist_ID)
375
+ .execute(num_retries=5)
376
+ )
356
377
 
357
378
  if "items" in response and len(response["items"]) > 0:
358
379
  playlist_owner = response["items"][0]["snippet"]["channelId"]
@@ -408,14 +429,14 @@ def compare_software_versions(version1, version2):
408
429
  Returns:
409
430
  (str): A comment on version1's relationship to version2 (i.e., older, newer, same or pre-release).
410
431
  """
411
- #Split version number and pre-release version
412
- split_version1 = version1.split('-')
413
- split_version2 = version2.split('-')
432
+ # Split version number and pre-release version
433
+ split_version1 = version1.split("-")
434
+ split_version2 = version2.split("-")
414
435
 
415
- ver_nb1,ver_rel1 = split_version1[0],split_version1[-1]
416
- ver_nb2,ver_rel2 = split_version2[0],split_version2[-1]
436
+ ver_nb1, ver_rel1 = split_version1[0], split_version1[-1]
437
+ ver_nb2, ver_rel2 = split_version2[0], split_version2[-1]
417
438
 
418
- if ver_nb1!=ver_rel1:
439
+ if ver_nb1 != ver_rel1:
419
440
  return "pre-release"
420
441
 
421
442
  arr1 = list(map(int, ver_nb1.split(".")))
QTube/utils/helpers.py CHANGED
@@ -4,14 +4,16 @@ import sys
4
4
  import time
5
5
 
6
6
  from googleapiclient.errors import HttpError
7
+ from colorama import Fore, Style
7
8
 
8
9
 
9
- def handle_http_errors(verbosity: list[str], func, *args, **kwargs):
10
+ def handle_http_errors(verbosity: list[str], fancy, func, *args, **kwargs):
10
11
  """Handles http errors when making API queries.
11
12
  If after 5 tries, the function could not be executed, it shuts the program down.
12
13
 
13
14
  Args:
14
15
  verbosity (list[str]): User defined verbosity.
16
+ fancy (bool): Determines wether the text is fancyfied (emoji+color).
15
17
  func (function): Function to be executed, with its arguments and keyword arguments.
16
18
  args (any): Arguments of func.
17
19
  kwargs (any): Keyword arguments of func.
@@ -25,7 +27,11 @@ def handle_http_errors(verbosity: list[str], func, *args, **kwargs):
25
27
  try:
26
28
  res = func(*args, **kwargs)
27
29
  print2(
28
- f"{func.__name__} successfully executed.", ["all", "func"], verbosity
30
+ f"{func.__name__} successfully executed.",
31
+ fancy,
32
+ "success",
33
+ ["all", "func"],
34
+ verbosity,
29
35
  )
30
36
  return res # Return the response if no error occurs
31
37
  except HttpError as err:
@@ -59,20 +65,54 @@ def handle_http_errors(verbosity: list[str], func, *args, **kwargs):
59
65
  sys.exit() # Exit the program after 5 retries
60
66
 
61
67
 
62
- def print2(message: str, verb_level: list, verbosity: list) -> None:
63
- """Prints text in the terminal depending on the choosen verbosity.
68
+ def fancify_text(text, color, style, emoji):
69
+ """Modifies the color and content of a string.
70
+
71
+ Args:
72
+ text (str): Original string.
73
+ color (colorama color): Colorama color to be applied to the text.
74
+ style (colorama style): Colorama style to be applied to the text.
75
+ emoji (str): Emoji to add at the beginning of the text.
76
+
77
+ Returns:
78
+ fancy_text (str): Fancified text.
79
+ """
80
+ fancy_text = f"{emoji}{style}{' - '}{color}{text}{Style.RESET_ALL}"
81
+
82
+ return fancy_text
83
+
84
+
85
+ def print2(
86
+ message: str, fancy: bool, fancy_type: None, verb_level: list, verbosity: list
87
+ ) -> None:
88
+ """Prints text in the terminal depending on the choosen verbosity and fancy type.
64
89
 
65
90
  Args:
66
91
  message (str): Text to be printed in the terminal.
92
+ fancy (bool): Determines wether the text is fancyfied (emoji+color).
93
+ fancy_type (str): Determines how the message is fancified (success, fail, warning, info or video).
67
94
  verb_level (list[str]): Verbosity associated to the text.
68
95
  verbosity (list[str]): User defined verbosity.
69
96
 
70
97
  Returns:
71
98
  None
72
99
  """
73
-
74
100
  if any(v in verb_level for v in verbosity):
75
- print(message)
101
+ if fancy:
102
+ if fancy_type == "success":
103
+ print(fancify_text(message, Fore.GREEN, Style.BRIGHT, "✅"))
104
+ elif fancy_type == "fail":
105
+ print(fancify_text(message, Fore.RED, Style.BRIGHT, "❌"))
106
+ elif fancy_type == "warning":
107
+ print(fancify_text(message, Fore.YELLOW, Style.BRIGHT, "⚠️"))
108
+ elif fancy_type == "info":
109
+ print(fancify_text(message, Fore.WHITE, Style.BRIGHT, "📢"))
110
+ elif fancy_type == "video":
111
+ print(fancify_text(message, Fore.BLUE, Style.BRIGHT, "🎞️"))
112
+ else:
113
+ print(message)
114
+ else:
115
+ print(message)
76
116
 
77
117
 
78
118
  def split_list(input_list: list, chunk_size: int) -> list:
@@ -187,3 +227,21 @@ def remove_multiple_spaces(text):
187
227
  (str): Same text string, but with multiple spaces replaced by a single space.
188
228
  """
189
229
  return re.sub(" +", " ", text)
230
+
231
+
232
+ def divide_lists(list1, list2, percentage: False):
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
@@ -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",
@@ -248,6 +288,14 @@ def parse_arguments():
248
288
  help="ID of the playlist the videos will be added to. Default: None",
249
289
  )
250
290
 
291
+ parser.add_argument(
292
+ "-fm",
293
+ "--fancy_mode",
294
+ metavar="",
295
+ type=str,
296
+ help="Enables fancy mode (colors and emojis) for terminal output. Default: True",
297
+ )
298
+
251
299
  parser.add_argument(
252
300
  "-v",
253
301
  "--verbosity",
@@ -259,6 +307,7 @@ def parse_arguments():
259
307
 
260
308
  return vars(parser.parse_args())
261
309
 
310
+
262
311
  def format_arguments(args_dict):
263
312
  """Formats the parsed command line arguments (written with the help of AI, regex is witchcraft to me).
264
313
 
@@ -270,15 +319,17 @@ def format_arguments(args_dict):
270
319
  """
271
320
  if co_str := args_dict.get("caption_options"):
272
321
  # Define a regex to match lists and split the values inside the brackets
273
- co_str2 = re.sub(r'(\w+):\s*\[([^\]]*)\]',
274
- lambda m: f'"{m.group(1)}": ["' + '", "'.join(m.group(2).split(',')) + '"]',
275
- co_str)
322
+ co_str2 = re.sub(
323
+ r"(\w+):\s*\[([^\]]*)\]",
324
+ lambda m: f'"{m.group(1)}": ["' + '", "'.join(m.group(2).split(",")) + '"]',
325
+ co_str,
326
+ )
276
327
 
277
328
  # Match booleans
278
- co_str2 = re.sub(r'(\w+):\s*(True|False)', r'"\1": \2', co_str2)
329
+ co_str2 = re.sub(r"(\w+):\s*(True|False)", r'"\1": \2', co_str2)
279
330
 
280
331
  # Match other key-value pairs
281
- co_str2 = re.sub(r'(\w+):\s*(\w+)', r'"\1": "\2"', co_str2)
332
+ co_str2 = re.sub(r"(\w+):\s*(\w+)", r'"\1": "\2"', co_str2)
282
333
 
283
334
  # Replace single quotes with double quotes to comply with JSON format
284
335
  co_str2 = co_str2.replace("'", '"')
@@ -9,7 +9,9 @@ def make_caption_requests(youtube, video_IDs: list[str]) -> dict[dict]:
9
9
  responses_dict (dict[dict]): Dictionary with video IDs as keys and YT API caption responses as values.
10
10
  """
11
11
  responses_dict = {
12
- video_ID: youtube.captions().list(part="snippet", videoId=video_ID).execute()
12
+ video_ID: youtube.captions()
13
+ .list(part="snippet", videoId=video_ID)
14
+ .execute(num_retries=5)
13
15
  for video_ID in video_IDs
14
16
  }
15
17
 
@@ -20,7 +20,7 @@ def get_subscriptions(youtube, next_page_token=None) -> dict:
20
20
  order="alphabetical",
21
21
  pageToken=next_page_token,
22
22
  )
23
- .execute()
23
+ .execute(num_retries=5)
24
24
  )
25
25
 
26
26
  for item in response.get("items", []):
@@ -48,7 +48,11 @@ def get_channel_info(youtube, handle: str):
48
48
  """
49
49
  channel = {}
50
50
 
51
- response = youtube.channels().list(part="snippet", forHandle=handle).execute()
51
+ response = (
52
+ youtube.channels()
53
+ .list(part="snippet", forHandle=handle)
54
+ .execute(num_retries=5)
55
+ )
52
56
 
53
57
  if "items" in response.keys():
54
58
  title = response["items"][0]["snippet"]["title"]
@@ -74,7 +78,9 @@ def get_uploads_playlists(youtube, channel_IDs: list[str]) -> list[str]:
74
78
  """
75
79
  channel_IDs_str = ",".join(channel_IDs)
76
80
  response = (
77
- youtube.channels().list(part="contentDetails", id=channel_IDs_str).execute()
81
+ youtube.channels()
82
+ .list(part="contentDetails", id=channel_IDs_str)
83
+ .execute(num_retries=5)
78
84
  )
79
85
  # Create a dictionary to store the mapping between channel IDs and upload playlist IDs
80
86
  channel_to_upload_map = {
@@ -100,7 +106,7 @@ def get_user_info(youtube) -> dict:
100
106
  response = (
101
107
  youtube.channels()
102
108
  .list(part="snippet,contentDetails,statistics", mine=True)
103
- .execute()
109
+ .execute(num_retries=5)
104
110
  )
105
111
 
106
112
  return response
@@ -14,7 +14,7 @@ def get_recent_videos(youtube, playlist_ID: str) -> dict:
14
14
  response = (
15
15
  youtube.playlistItems()
16
16
  .list(part="contentDetails", playlistId=playlist_ID, maxResults=5)
17
- .execute()
17
+ .execute(num_retries=5)
18
18
  )
19
19
 
20
20
  recent_vids = {
@@ -50,7 +50,7 @@ def get_playlist_content(youtube, playlist_ID: str) -> list[str]:
50
50
  maxResults=50,
51
51
  pageToken=next_page_token,
52
52
  )
53
- .execute()
53
+ .execute(num_retries=5)
54
54
  )
55
55
 
56
56
  temp_videos_IDs = [
@@ -77,13 +77,39 @@ def get_playlists_titles(youtube=None, playlist_IDs: list[str] = None):
77
77
  titles (list[str]): List of YT playlist titles.
78
78
  """
79
79
  playlist_IDs_str = ",".join(playlist_IDs)
80
- response = youtube.playlists().list(part="snippet", id=playlist_IDs_str).execute()
80
+ response = (
81
+ youtube.playlists()
82
+ .list(part="snippet", id=playlist_IDs_str)
83
+ .execute(num_retries=5)
84
+ )
81
85
 
82
86
  titles = [playlist["snippet"]["title"] for playlist in response["items"]]
83
87
 
84
88
  return titles
85
89
 
86
90
 
91
+ def get_playlists_video_counts(youtube=None, playlist_IDs: list[str] = None):
92
+ """Retrieves the number of videos of a list of YT playlists.
93
+
94
+ Args:
95
+ youtube (Resource): YT API resource.
96
+ playlists_IDs (list[str]): List of playlist IDs.
97
+
98
+ Returns:
99
+ counts (list[int]): List of YT playlist video counts.
100
+ """
101
+ playlist_IDs_str = ",".join(playlist_IDs)
102
+ response = (
103
+ youtube.playlists()
104
+ .list(part="contentDetails", id=playlist_IDs_str)
105
+ .execute(num_retries=5)
106
+ )
107
+
108
+ counts = [playlist["contentDetails"]["itemCount"] for playlist in response["items"]]
109
+
110
+ return counts
111
+
112
+
87
113
  def add_to_playlist(youtube, playlist_ID: str, video_ID: str) -> None:
88
114
  """Adds a YT video to the YT playlist.
89
115
 
@@ -106,6 +132,6 @@ def add_to_playlist(youtube, playlist_ID: str, video_ID: str) -> None:
106
132
  }
107
133
  },
108
134
  )
109
- .execute()
135
+ .execute(num_retries=5)
110
136
  )
111
137
  return
@@ -2,6 +2,8 @@ import isodate
2
2
 
3
3
  from pytube import YouTube
4
4
 
5
+ from QTube.utils import helpers
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.
@@ -15,7 +17,9 @@ def make_video_requests(youtube, video_IDs: list[str]) -> dict:
15
17
  """
16
18
  video_IDs_str = ",".join(video_IDs)
17
19
  response = (
18
- youtube.videos().list(part="snippet,contentDetails", id=video_IDs_str).execute()
20
+ youtube.videos()
21
+ .list(part="snippet,contentDetails,statistics", id=video_IDs_str)
22
+ .execute(num_retries=5)
19
23
  )
20
24
  return response
21
25
 
@@ -39,7 +43,11 @@ def get_titles(
39
43
  """
40
44
  if use_API:
41
45
  video_IDs_str = ",".join(video_IDs)
42
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
46
+ response = (
47
+ youtube.videos()
48
+ .list(part="snippet", id=video_IDs_str)
49
+ .execute(num_retries=5)
50
+ )
43
51
 
44
52
  titles = [vid["snippet"]["title"] for vid in response["items"]]
45
53
 
@@ -65,7 +73,11 @@ def get_tags(
65
73
  """
66
74
  if use_API:
67
75
  video_IDs_str = ",".join(video_IDs)
68
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
76
+ response = (
77
+ youtube.videos()
78
+ .list(part="snippet", id=video_IDs_str)
79
+ .execute(num_retries=5)
80
+ )
69
81
 
70
82
  tags = [
71
83
  vid["snippet"]["tags"] if "tags" in vid["snippet"] else None
@@ -94,7 +106,11 @@ def get_descriptions(
94
106
  """
95
107
  if use_API:
96
108
  video_IDs_str = ",".join(video_IDs) # Join the video IDs with commas
97
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
109
+ response = (
110
+ youtube.videos()
111
+ .list(part="snippet", id=video_IDs_str)
112
+ .execute(num_retries=5)
113
+ )
98
114
 
99
115
  descriptions = [vid["snippet"]["description"] for vid in response.get("items", [])]
100
116
  return descriptions
@@ -120,7 +136,9 @@ def get_durations(
120
136
  if use_API:
121
137
  video_IDs_str = ",".join(video_IDs)
122
138
  response = (
123
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
139
+ youtube.videos()
140
+ .list(part="contentDetails", id=video_IDs_str)
141
+ .execute(num_retries=5)
124
142
  )
125
143
 
126
144
  durations_iso = [
@@ -150,7 +168,11 @@ def get_languages(
150
168
  """
151
169
  if use_API:
152
170
  video_IDs_str = ",".join(video_IDs)
153
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
171
+ response = (
172
+ youtube.videos()
173
+ .list(part="snippet", id=video_IDs_str)
174
+ .execute(num_retries=5)
175
+ )
154
176
 
155
177
  languages = [
156
178
  (
@@ -190,7 +212,9 @@ def get_dimensions(
190
212
  if use_API:
191
213
  video_IDs_str = ",".join(video_IDs)
192
214
  response = (
193
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
215
+ youtube.videos()
216
+ .list(part="contentDetails", id=video_IDs_str)
217
+ .execute(num_retries=5)
194
218
  )
195
219
 
196
220
  dimensions = [vid["contentDetails"]["dimension"] for vid in response["items"]]
@@ -217,7 +241,9 @@ def get_definitions(
217
241
  if use_API:
218
242
  video_IDs_str = ",".join(video_IDs)
219
243
  response = (
220
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
244
+ youtube.videos()
245
+ .list(part="contentDetails", id=video_IDs_str)
246
+ .execute(num_retries=5)
221
247
  )
222
248
 
223
249
  definitions = [vid["contentDetails"]["definition"] for vid in response["items"]]
@@ -305,13 +331,167 @@ def get_projections(
305
331
  if use_API:
306
332
  video_IDs_str = ",".join(video_IDs)
307
333
  response = (
308
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
334
+ youtube.videos()
335
+ .list(part="contentDetails", id=video_IDs_str)
336
+ .execute(num_retries=5)
309
337
  )
310
338
 
311
339
  projections = [vid["contentDetails"]["projection"] for vid in response["items"]]
312
340
  return projections
313
341
 
314
342
 
343
+ def get_view_counts(
344
+ youtube=None,
345
+ response: dict = None,
346
+ video_IDs: list[str] = None,
347
+ use_API: bool = False,
348
+ ) -> list[str]:
349
+ """Retrieves the number of views of a list of YT videos.
350
+
351
+ Args:
352
+ youtube (Resource): YT API resource.
353
+ response (dict[dict]): YT API response from the make_video_request function.
354
+ video_IDs (list[str]): List of video IDs.
355
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
356
+
357
+ Returns:
358
+ views (list[int]): List of YT videos views.
359
+ """
360
+ if use_API:
361
+ video_IDs_str = ",".join(video_IDs)
362
+ response = (
363
+ youtube.videos()
364
+ .list(part="statistics", id=video_IDs_str)
365
+ .execute(num_retries=5)
366
+ )
367
+
368
+ views = [int(vid["statistics"]["viewCount"]) for vid in response["items"]]
369
+
370
+ return views
371
+
372
+
373
+ def get_like_counts(
374
+ youtube=None,
375
+ response: dict = None,
376
+ video_IDs: list[str] = None,
377
+ use_API: bool = False,
378
+ ) -> list[str]:
379
+ """Retrieves the number of likes of a list of YT videos.
380
+
381
+ Args:
382
+ youtube (Resource): YT API resource.
383
+ response (dict[dict]): YT API response from the make_video_request function.
384
+ video_IDs (list[str]): List of video IDs.
385
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
386
+
387
+ Returns:
388
+ likes (list[int]): List of YT videos likes.
389
+ """
390
+ if use_API:
391
+ video_IDs_str = ",".join(video_IDs)
392
+ response = (
393
+ youtube.videos()
394
+ .list(part="statistics", id=video_IDs_str)
395
+ .execute(num_retries=5)
396
+ )
397
+
398
+ likes = [int(vid["statistics"]["likeCount"]) for vid in response["items"]]
399
+
400
+ return likes
401
+
402
+
403
+ def get_comment_counts(
404
+ youtube=None,
405
+ response: dict = None,
406
+ video_IDs: list[str] = None,
407
+ use_API: bool = False,
408
+ ) -> list[str]:
409
+ """Retrieves the number of comments of a list of YT videos.
410
+
411
+ Args:
412
+ youtube (Resource): YT API resource.
413
+ response (dict[dict]): YT API response from the make_video_request function.
414
+ video_IDs (list[str]): List of video IDs.
415
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
416
+
417
+ Returns:
418
+ comment_counts (list[int]): List of YT videos comment counts.
419
+ """
420
+ if use_API:
421
+ video_IDs_str = ",".join(video_IDs)
422
+ response = (
423
+ youtube.videos()
424
+ .list(part="statistics", id=video_IDs_str)
425
+ .execute(num_retries=5)
426
+ )
427
+
428
+ comment_counts = [
429
+ int(vid["statistics"]["commentCount"]) for vid in response["items"]
430
+ ]
431
+
432
+ return comment_counts
433
+
434
+
435
+ def get_likes_to_views_ratio(
436
+ likes,
437
+ views,
438
+ youtube=None,
439
+ response: dict = None,
440
+ video_IDs: list[str] = None,
441
+ use_API: bool = False,
442
+ ) -> list[str]:
443
+ """Retrieves the likes to views ratio of a list of YT videos.
444
+
445
+ Args:
446
+ likes (list[int]): List of the number of likes.
447
+ views (list[int]): List of the number of views.
448
+ youtube (Resource): YT API resource.
449
+ response (dict[dict]): YT API response from the make_video_request function.
450
+ video_IDs (list[str]): List of video IDs.
451
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
452
+
453
+ Returns:
454
+ ratio (list[int|float]): List of YT videos' likes to views ratios.
455
+ """
456
+ if use_API:
457
+ views = get_view_counts(youtube, response, video_IDs, use_API)
458
+ likes = get_like_counts(youtube, response, video_IDs, use_API)
459
+
460
+ ratio = helpers.divide_lists(likes, views, False)
461
+
462
+ return ratio
463
+
464
+
465
+ def get_comments_to_views_ratio(
466
+ likes,
467
+ views,
468
+ youtube=None,
469
+ response: dict = None,
470
+ video_IDs: list[str] = None,
471
+ use_API: bool = False,
472
+ ) -> list[str]:
473
+ """Retrieves the comments to views ratio of a list of YT videos.
474
+
475
+ Args:
476
+ comments (list[int]): List of the number of comments.
477
+ views (list[int]): List of the number of views.
478
+ youtube (Resource): YT API resource.
479
+ response (dict[dict]): YT API response from the make_video_request function.
480
+ video_IDs (list[str]): List of video IDs.
481
+ use_API (bool): Determines if a new API request is made or if the response dictionary is used.
482
+
483
+ Returns:
484
+ views (list[int|float]): List of YT videos' comments to views ratios.
485
+ """
486
+ if use_API:
487
+ views = get_view_counts(youtube, response, video_IDs, use_API)
488
+ comments = get_comment_counts(youtube, response, video_IDs, use_API)
489
+
490
+ ratio = helpers.divide_lists(comments, views, False)
491
+
492
+ return ratio
493
+
494
+
315
495
  def has_captions(
316
496
  youtube=None,
317
497
  response: dict = None,
@@ -332,7 +512,9 @@ def has_captions(
332
512
  if use_API:
333
513
  video_IDs_str = ",".join(video_IDs)
334
514
  response = (
335
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
515
+ youtube.videos()
516
+ .list(part="contentDetails", id=video_IDs_str)
517
+ .execute(num_retries=5)
336
518
  )
337
519
 
338
520
  captions = [vid["contentDetails"]["caption"] for vid in response["items"]]
@@ -382,7 +564,11 @@ def is_live(
382
564
  """
383
565
  if use_API:
384
566
  video_IDs_str = ",".join(video_IDs)
385
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
567
+ response = (
568
+ youtube.videos()
569
+ .list(part="snippet", id=video_IDs_str)
570
+ .execute(num_retries=5)
571
+ )
386
572
 
387
573
  live_statuses = [
388
574
  vid["snippet"]["liveBroadcastContent"] for vid in response["items"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: QTube
3
- Version: 2.1.1
3
+ Version: 2.3.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
@@ -44,6 +44,7 @@ Classifier: Topic :: Multimedia :: Video
44
44
  Requires-Python: >=3.8
45
45
  Description-Content-Type: text/markdown
46
46
  License-File: LICENSE.txt
47
+ Requires-Dist: colorama >=0.4.6
47
48
  Requires-Dist: google-api-python-client >=2.119.0
48
49
  Requires-Dist: google-auth-oauthlib >=1.0.0
49
50
  Requires-Dist: isodate >=0.6.1
@@ -97,6 +98,7 @@ Each of these rules is based on putting some kind of constraint on video propert
97
98
  * Language filtering
98
99
  * Caption filtering
99
100
  * Duration filtering
101
+ * Views, likes & comments counts filtering
100
102
  * Livestream filtering
101
103
  * Premiere filtering
102
104
  * Quality filtering
@@ -144,11 +146,17 @@ For more versatile uses, you can also use command line arguments with the [qtube
144
146
  |`lowest_framerate`|Yes|Minimum framerate. Videos with framerates stricly lower than this value will not be added.|Positive integer|
145
147
  |`preferred_dimensions`|Yes|Dimension the videos need to be in.|*2D*, *3D* or both|
146
148
  |`preferred_projections`|Yes|Projection the videos need to be in.|*rectangular*, *360* or both|
149
+ |`views_threshold`|No|Minimum number of times videos have been viewed.|Positive integer|
150
+ |`likes_threshold`|No|Minimum number of times videos have been liked.|Positive integer|
151
+ |`comments_threshold`|No|Minimum number of times videos have been commented on.|Positive integer|
152
+ |`likes_to_views_ratio`|No|Minimum likes to views ratio.|Positive float between 0 & 1|
153
+ |`comments_to_views_ratio`|No|Minimum comments to views ratio.|Positive float between 0 & 1|
147
154
  |`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|
148
155
  |`keep_shorts`|No|Determines whether to add shorts.|boolean|
149
156
  |`keep_duplicates`|No|Determines whether to add videos that are already in the playlist.|boolean|
150
157
  |`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|
151
158
  |`override_json`|No|Allow command line arguments to override user_params.json parameters.|boolean|
159
+ |`fancy_mode`|No|Enables fancy mode (colors and emojis) for terminal output. |boolean|
152
160
  |`verbosity`|No|Controls how much information is shown in the terminal. Options can be combined, so that selecting each option gives the same result as selecting *all*. <br>1: Everything is shown.<br>2: Nothing is shown.<br>3: Only information regarding function execution is shown.<br>4: Only information regarding credentials is shown (loading, retrieving and saving).<br>5: Only information regarding added videos is shown (number, channel names and video titles).|<br>*all*<sup> 1 </sup>, <br>*none*<sup> 2 </sup> , <br>*func*<sup> 3 </sup>, <br>*credentials*<sup> 4 </sup> ,<br>*videos*<sup> 5 </sup>.|
153
161
 
154
162
  All parameters are case-sensitive by default and if you do not want to use an optional parameter, replace its value with *null* or delete the entry.
@@ -191,11 +199,17 @@ The following *user_params.json* file would add every new videos from channels y
191
199
  "lowest_framerate": null,
192
200
  "preferred_dimensions": null,
193
201
  "preferred_projections": null,
202
+ "views_threshold": 0,
203
+ "likes_threshold": 0,
204
+ "comments_threshold": 0,
205
+ "likes_to_views_ratio": 0,
206
+ "comments_to_views_ratio": 0,
194
207
  "run_frequency":"daily",
195
208
  "keep_shorts": true,
196
209
  "keep_duplicates": false,
197
210
  "upload_playlist_ID": "your_playlist_ID",
198
211
  "override_json":false,
212
+ "fancy_mode":true,
199
213
  "verbosity": ["credentials","videos"]
200
214
  }
201
215
  ```
@@ -227,11 +241,17 @@ The following *user_params.json* file would only add videos with good quality.
227
241
  "lowest_framerate": 60,
228
242
  "preferred_dimensions": ["2D"],
229
243
  "preferred_projections": ["rectangular"],
244
+ "views_threshold": 0,
245
+ "likes_threshold": 0,
246
+ "comments_threshold": 0,
247
+ "likes_to_views_ratio": 0,
248
+ "comments_to_views_ratio": 0,
230
249
  "run_frequency":"daily",
231
250
  "keep_shorts": true,
232
251
  "keep_duplicates": false,
233
252
  "upload_playlist_ID": "your_playlist_ID",
234
253
  "override_json":false,
254
+ "fancy_mode":true,
235
255
  "verbosity": ["credentials","videos"]
236
256
  }
237
257
  ```
@@ -263,11 +283,17 @@ The following *user_params.json* file would only add the *$1 vs.* MrBeast videos
263
283
  "lowest_framerate": null,
264
284
  "preferred_dimensions": ["2D"],
265
285
  "preferred_projections": ["rectangular"],
286
+ "views_threshold": 0,
287
+ "likes_threshold": 0,
288
+ "comments_threshold": 0,
289
+ "likes_to_views_ratio": 0,
290
+ "comments_to_views_ratio": 0,
266
291
  "run_frequency":"daily",
267
292
  "keep_shorts": false,
268
293
  "keep_duplicates": false,
269
294
  "upload_playlist_ID": "your_playlist_ID",
270
295
  "override_json":false,
296
+ "fancy_mode":true,
271
297
  "verbosity": ["credentials","videos"]
272
298
  }
273
299
  ```
@@ -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=wzEkhjGgmRFtN9kNBZfqd_6rklX11hwzadh_DAQGZpM,33916
4
+ QTube/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ QTube/utils/checks.py,sha256=wq__Xj_NEZa9tjliRTh-kZcHHbll0rqdjBXwIfZso9E,12569
6
+ QTube/utils/helpers.py,sha256=cOhy4hauguO5-eJK9pK7NvPOtF5ZWp4zfFipA-DY1QM,9131
7
+ QTube/utils/parsing.py,sha256=Q9GnSZSKwX7-AVQDlIoXlQ4xIgQIwMZfOIKXZWi7xSk,10359
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=lHl-MrloJODCbGD38lwrPQmyEE8t-ZvLA0B4gfMSG0c,18159
12
+ QTube-2.3.0.dist-info/LICENSE.txt,sha256=cctyZoZUseENxkeq5p_C5oCFYpK0-ECku_sCZmWbL0E,1098
13
+ QTube-2.3.0.dist-info/METADATA,sha256=Ftm4t0X_hgyJelTS1v46YehDTPbZglWR5Hp4d3iMIio,17265
14
+ QTube-2.3.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
15
+ QTube-2.3.0.dist-info/entry_points.txt,sha256=Q3SLDyRahuzrNnHC3UOkMH5gM_Um3eCLiLG4vSTPjqE,51
16
+ QTube-2.3.0.dist-info/top_level.txt,sha256=z6oXrT8BiTTZuygjsbfe-NE4rmkHlDK8euAL9m6BT4A,6
17
+ QTube-2.3.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=02FnIYdGsOzLAUqtnzy-8XCHQF7W0CyoPrRspvgdtRE,27150
4
- QTube/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- QTube/utils/checks.py,sha256=6EGmI37koeyLOEOWtE7g2RgEceMEjzgXPCK0eV_bnOc,11682
6
- QTube/utils/helpers.py,sha256=JPftVRBpOoV8PrwTDdaqCHFhHNaEGQAudHZQgfRq1fo,6833
7
- QTube/utils/parsing.py,sha256=SAVrm-LK_wZQ4HmWNe-b3rxdSdac-i7KTzQxsQhLT_E,9237
8
- QTube/utils/youtube/captions.py,sha256=9p5VcGESY3KPS3aDvoVyHLfQhiYnDdfkvJWOxcYv7ZM,1558
9
- QTube/utils/youtube/channels.py,sha256=TJi2aasZuCgKKjPSBC5Hki8fK0wlWqwSTyyMF7ctVNU,3233
10
- QTube/utils/youtube/playlists.py,sha256=gu9pMTzMI0Z6UyCZkjoxA1TiBfhlqySLJXcTeBOAh2g,3090
11
- QTube/utils/youtube/videos.py,sha256=lepukLiX5pP8t2q9XI66_oPXDbu4Nxf0pDm9dxwbP-U,12709
12
- QTube-2.1.1.dist-info/LICENSE.txt,sha256=cctyZoZUseENxkeq5p_C5oCFYpK0-ECku_sCZmWbL0E,1098
13
- QTube-2.1.1.dist-info/METADATA,sha256=4Fdf2p-zsX0MC2HGGpiXZwED9Wq1oys-glWX3DhT4mA,16187
14
- QTube-2.1.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
15
- QTube-2.1.1.dist-info/entry_points.txt,sha256=Q3SLDyRahuzrNnHC3UOkMH5gM_Um3eCLiLG4vSTPjqE,51
16
- QTube-2.1.1.dist-info/top_level.txt,sha256=z6oXrT8BiTTZuygjsbfe-NE4rmkHlDK8euAL9m6BT4A,6
17
- QTube-2.1.1.dist-info/RECORD,,
File without changes