QTube 2.1.1__py3-none-any.whl → 2.2.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(),
@@ -339,6 +429,7 @@ def main():
339
429
  if need_captions:
340
430
  captions_responses = QTube.utils.helpers.handle_http_errors(
341
431
  verb,
432
+ fancy,
342
433
  QTube.utils.youtube.captions.make_caption_requests,
343
434
  youtube,
344
435
  videos.keys(),
@@ -689,29 +780,66 @@ def main():
689
780
  playlist_title = QTube.utils.youtube.playlists.get_playlists_titles(
690
781
  youtube, [playlist_ID]
691
782
  )[0]
783
+ playlist_video_count = QTube.utils.youtube.playlists.get_playlists_video_counts(
784
+ youtube, [playlist_ID]
785
+ )[0]
786
+
692
787
  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(
788
+ if (
789
+ playlist_video_count + len(videos_to_add) > 5000
790
+ ): # Checks if current video count + new videos would exceed 5k (YT playlist size limit)
791
+ QTube.utils.helpers.print2(
792
+ f"The {playlist_title} playlist would reach or exceed the 5000 size limit if the following videos were added to it:",
793
+ fancy,
794
+ "fail",
795
+ ["all", "videos"],
700
796
  verb,
701
- QTube.utils.youtube.playlists.add_to_playlist,
702
- youtube,
703
- playlist_ID,
704
- vid_ID,
705
797
  )
706
-
798
+ for vid_ID, vid_info in videos_to_add.items():
799
+ QTube.utils.helpers.print2(
800
+ 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}",
801
+ fancy,
802
+ "video",
803
+ ["all", "videos"],
804
+ verb,
805
+ )
707
806
  QTube.utils.helpers.print2(
708
- f"From {vid_info['channel name']}, the video named: {vid_info['original title']} has been added.",
807
+ f"Remove at least {len(videos_to_add)} videos from the {playlist_title} playlist so that the new one(s) can be added.",
808
+ fancy,
809
+ "warning",
709
810
  ["all", "videos"],
710
811
  verb,
711
812
  )
813
+ else:
814
+ QTube.utils.helpers.print2(
815
+ f"The following videos will be added to the {playlist_title} playlist:",
816
+ fancy,
817
+ "info",
818
+ ["all", "videos"],
819
+ verb,
820
+ )
821
+ for vid_ID, vid_info in videos_to_add.items():
822
+ QTube.utils.helpers.handle_http_errors(
823
+ verb,
824
+ fancy,
825
+ QTube.utils.youtube.playlists.add_to_playlist,
826
+ youtube,
827
+ playlist_ID,
828
+ vid_ID,
829
+ )
830
+
831
+ QTube.utils.helpers.print2(
832
+ f"From {vid_info['channel name']}, the video named: {vid_info['original title']} has been added.",
833
+ fancy,
834
+ "video",
835
+ ["all", "videos"],
836
+ verb,
837
+ )
712
838
  else:
713
839
  QTube.utils.helpers.print2(
714
840
  f"No new videos to add to the {playlist_title} playlist.",
841
+ fancy,
842
+ "info",
715
843
  ["all", "videos"],
716
844
  verb,
717
845
  )
QTube/utils/checks.py CHANGED
@@ -332,6 +332,8 @@ 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),
335
337
  ]
336
338
 
337
339
  ok = all(checks)
@@ -352,7 +354,11 @@ def check_playlist_id(youtube, user_info: dict, test_playlist_ID: str) -> bool:
352
354
  """
353
355
  user_channel_ID = user_info["items"][0]["id"]
354
356
 
355
- response = youtube.playlists().list(part="snippet", id=test_playlist_ID).execute()
357
+ response = (
358
+ youtube.playlists()
359
+ .list(part="snippet", id=test_playlist_ID)
360
+ .execute(num_retries=5)
361
+ )
356
362
 
357
363
  if "items" in response and len(response["items"]) > 0:
358
364
  playlist_owner = response["items"][0]["snippet"]["channelId"]
@@ -408,14 +414,14 @@ def compare_software_versions(version1, version2):
408
414
  Returns:
409
415
  (str): A comment on version1's relationship to version2 (i.e., older, newer, same or pre-release).
410
416
  """
411
- #Split version number and pre-release version
412
- split_version1 = version1.split('-')
413
- split_version2 = version2.split('-')
417
+ # Split version number and pre-release version
418
+ split_version1 = version1.split("-")
419
+ split_version2 = version2.split("-")
414
420
 
415
- ver_nb1,ver_rel1 = split_version1[0],split_version1[-1]
416
- ver_nb2,ver_rel2 = split_version2[0],split_version2[-1]
421
+ ver_nb1, ver_rel1 = split_version1[0], split_version1[-1]
422
+ ver_nb2, ver_rel2 = split_version2[0], split_version2[-1]
417
423
 
418
- if ver_nb1!=ver_rel1:
424
+ if ver_nb1 != ver_rel1:
419
425
  return "pre-release"
420
426
 
421
427
  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,7 @@ 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.",fancy,"success", ["all", "func"], verbosity
29
31
  )
30
32
  return res # Return the response if no error occurs
31
33
  except HttpError as err:
@@ -59,20 +61,54 @@ def handle_http_errors(verbosity: list[str], func, *args, **kwargs):
59
61
  sys.exit() # Exit the program after 5 retries
60
62
 
61
63
 
62
- def print2(message: str, verb_level: list, verbosity: list) -> None:
63
- """Prints text in the terminal depending on the choosen verbosity.
64
+ def fancify_text(text, color, style, emoji):
65
+ """Modifies the color and content of a string.
66
+
67
+ Args:
68
+ text (str): Original string.
69
+ color (colorama color): Colorama color to be applied to the text.
70
+ style (colorama style): Colorama style to be applied to the text.
71
+ emoji (str): Emoji to add at the beginning of the text.
72
+
73
+ Returns:
74
+ fancy_text (str): Fancified text.
75
+ """
76
+ fancy_text = f"{emoji}{style}{' - '}{color}{text}{Style.RESET_ALL}"
77
+
78
+ return fancy_text
79
+
80
+
81
+ def print2(
82
+ message: str, fancy: bool, fancy_type: None, verb_level: list, verbosity: list
83
+ ) -> None:
84
+ """Prints text in the terminal depending on the choosen verbosity and fancy type.
64
85
 
65
86
  Args:
66
87
  message (str): Text to be printed in the terminal.
88
+ fancy (bool): Determines wether the text is fancyfied (emoji+color).
89
+ fancy_type (str): Determines how the message is fancified (success, fail, warning, info or video).
67
90
  verb_level (list[str]): Verbosity associated to the text.
68
91
  verbosity (list[str]): User defined verbosity.
69
92
 
70
93
  Returns:
71
94
  None
72
95
  """
73
-
74
96
  if any(v in verb_level for v in verbosity):
75
- print(message)
97
+ if fancy:
98
+ if fancy_type == "success":
99
+ print(fancify_text(message, Fore.GREEN, Style.BRIGHT, "✅"))
100
+ elif fancy_type == "fail":
101
+ print(fancify_text(message, Fore.RED, Style.BRIGHT, "❌"))
102
+ elif fancy_type == "warning":
103
+ print(fancify_text(message, Fore.YELLOW, Style.NORMAL, "⚠️"))
104
+ elif fancy_type == "info":
105
+ print(fancify_text(message, Fore.WHITE, Style.BRIGHT,"📢"))
106
+ elif fancy_type == "video":
107
+ print(fancify_text(message, Fore.BLUE, Style.BRIGHT, "🎞️"))
108
+ else:
109
+ print(message)
110
+ else:
111
+ print(message)
76
112
 
77
113
 
78
114
  def split_list(input_list: list, chunk_size: int) -> list:
QTube/utils/parsing.py CHANGED
@@ -248,6 +248,14 @@ def parse_arguments():
248
248
  help="ID of the playlist the videos will be added to. Default: None",
249
249
  )
250
250
 
251
+ parser.add_argument(
252
+ "-fm",
253
+ "--fancy_mode",
254
+ metavar="",
255
+ type=str,
256
+ help="Enables fancy mode (colors and emojis) for terminal output. Default: True",
257
+ )
258
+
251
259
  parser.add_argument(
252
260
  "-v",
253
261
  "--verbosity",
@@ -259,6 +267,7 @@ def parse_arguments():
259
267
 
260
268
  return vars(parser.parse_args())
261
269
 
270
+
262
271
  def format_arguments(args_dict):
263
272
  """Formats the parsed command line arguments (written with the help of AI, regex is witchcraft to me).
264
273
 
@@ -270,15 +279,17 @@ def format_arguments(args_dict):
270
279
  """
271
280
  if co_str := args_dict.get("caption_options"):
272
281
  # 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)
282
+ co_str2 = re.sub(
283
+ r"(\w+):\s*\[([^\]]*)\]",
284
+ lambda m: f'"{m.group(1)}": ["' + '", "'.join(m.group(2).split(",")) + '"]',
285
+ co_str,
286
+ )
276
287
 
277
288
  # Match booleans
278
- co_str2 = re.sub(r'(\w+):\s*(True|False)', r'"\1": \2', co_str2)
289
+ co_str2 = re.sub(r"(\w+):\s*(True|False)", r'"\1": \2', co_str2)
279
290
 
280
291
  # Match other key-value pairs
281
- co_str2 = re.sub(r'(\w+):\s*(\w+)', r'"\1": "\2"', co_str2)
292
+ co_str2 = re.sub(r"(\w+):\s*(\w+)", r'"\1": "\2"', co_str2)
282
293
 
283
294
  # Replace single quotes with double quotes to comply with JSON format
284
295
  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
@@ -15,7 +15,9 @@ def make_video_requests(youtube, video_IDs: list[str]) -> dict:
15
15
  """
16
16
  video_IDs_str = ",".join(video_IDs)
17
17
  response = (
18
- youtube.videos().list(part="snippet,contentDetails", id=video_IDs_str).execute()
18
+ youtube.videos()
19
+ .list(part="snippet,contentDetails", id=video_IDs_str)
20
+ .execute(num_retries=5)
19
21
  )
20
22
  return response
21
23
 
@@ -39,7 +41,11 @@ def get_titles(
39
41
  """
40
42
  if use_API:
41
43
  video_IDs_str = ",".join(video_IDs)
42
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
44
+ response = (
45
+ youtube.videos()
46
+ .list(part="snippet", id=video_IDs_str)
47
+ .execute(num_retries=5)
48
+ )
43
49
 
44
50
  titles = [vid["snippet"]["title"] for vid in response["items"]]
45
51
 
@@ -65,7 +71,11 @@ def get_tags(
65
71
  """
66
72
  if use_API:
67
73
  video_IDs_str = ",".join(video_IDs)
68
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
74
+ response = (
75
+ youtube.videos()
76
+ .list(part="snippet", id=video_IDs_str)
77
+ .execute(num_retries=5)
78
+ )
69
79
 
70
80
  tags = [
71
81
  vid["snippet"]["tags"] if "tags" in vid["snippet"] else None
@@ -94,7 +104,11 @@ def get_descriptions(
94
104
  """
95
105
  if use_API:
96
106
  video_IDs_str = ",".join(video_IDs) # Join the video IDs with commas
97
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
107
+ response = (
108
+ youtube.videos()
109
+ .list(part="snippet", id=video_IDs_str)
110
+ .execute(num_retries=5)
111
+ )
98
112
 
99
113
  descriptions = [vid["snippet"]["description"] for vid in response.get("items", [])]
100
114
  return descriptions
@@ -120,7 +134,9 @@ def get_durations(
120
134
  if use_API:
121
135
  video_IDs_str = ",".join(video_IDs)
122
136
  response = (
123
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
137
+ youtube.videos()
138
+ .list(part="contentDetails", id=video_IDs_str)
139
+ .execute(num_retries=5)
124
140
  )
125
141
 
126
142
  durations_iso = [
@@ -150,7 +166,11 @@ def get_languages(
150
166
  """
151
167
  if use_API:
152
168
  video_IDs_str = ",".join(video_IDs)
153
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
169
+ response = (
170
+ youtube.videos()
171
+ .list(part="snippet", id=video_IDs_str)
172
+ .execute(num_retries=5)
173
+ )
154
174
 
155
175
  languages = [
156
176
  (
@@ -190,7 +210,9 @@ def get_dimensions(
190
210
  if use_API:
191
211
  video_IDs_str = ",".join(video_IDs)
192
212
  response = (
193
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
213
+ youtube.videos()
214
+ .list(part="contentDetails", id=video_IDs_str)
215
+ .execute(num_retries=5)
194
216
  )
195
217
 
196
218
  dimensions = [vid["contentDetails"]["dimension"] for vid in response["items"]]
@@ -217,7 +239,9 @@ def get_definitions(
217
239
  if use_API:
218
240
  video_IDs_str = ",".join(video_IDs)
219
241
  response = (
220
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
242
+ youtube.videos()
243
+ .list(part="contentDetails", id=video_IDs_str)
244
+ .execute(num_retries=5)
221
245
  )
222
246
 
223
247
  definitions = [vid["contentDetails"]["definition"] for vid in response["items"]]
@@ -305,7 +329,9 @@ def get_projections(
305
329
  if use_API:
306
330
  video_IDs_str = ",".join(video_IDs)
307
331
  response = (
308
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
332
+ youtube.videos()
333
+ .list(part="contentDetails", id=video_IDs_str)
334
+ .execute(num_retries=5)
309
335
  )
310
336
 
311
337
  projections = [vid["contentDetails"]["projection"] for vid in response["items"]]
@@ -332,7 +358,9 @@ def has_captions(
332
358
  if use_API:
333
359
  video_IDs_str = ",".join(video_IDs)
334
360
  response = (
335
- youtube.videos().list(part="contentDetails", id=video_IDs_str).execute()
361
+ youtube.videos()
362
+ .list(part="contentDetails", id=video_IDs_str)
363
+ .execute(num_retries=5)
336
364
  )
337
365
 
338
366
  captions = [vid["contentDetails"]["caption"] for vid in response["items"]]
@@ -382,7 +410,11 @@ def is_live(
382
410
  """
383
411
  if use_API:
384
412
  video_IDs_str = ",".join(video_IDs)
385
- response = youtube.videos().list(part="snippet", id=video_IDs_str).execute()
413
+ response = (
414
+ youtube.videos()
415
+ .list(part="snippet", id=video_IDs_str)
416
+ .execute(num_retries=5)
417
+ )
386
418
 
387
419
  live_statuses = [
388
420
  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.2.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
@@ -149,6 +150,7 @@ For more versatile uses, you can also use command line arguments with the [qtube
149
150
  |`keep_duplicates`|No|Determines whether to add videos that are already in the playlist.|boolean|
150
151
  |`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
152
  |`override_json`|No|Allow command line arguments to override user_params.json parameters.|boolean|
153
+ |`fancy_mode`|No|Enables fancy mode (colors and emojis) for terminal output. |boolean|
152
154
  |`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
155
 
154
156
  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.
@@ -196,6 +198,7 @@ The following *user_params.json* file would add every new videos from channels y
196
198
  "keep_duplicates": false,
197
199
  "upload_playlist_ID": "your_playlist_ID",
198
200
  "override_json":false,
201
+ "fancy_mode":true,
199
202
  "verbosity": ["credentials","videos"]
200
203
  }
201
204
  ```
@@ -232,6 +235,7 @@ The following *user_params.json* file would only add videos with good quality.
232
235
  "keep_duplicates": false,
233
236
  "upload_playlist_ID": "your_playlist_ID",
234
237
  "override_json":false,
238
+ "fancy_mode":true,
235
239
  "verbosity": ["credentials","videos"]
236
240
  }
237
241
  ```
@@ -268,6 +272,7 @@ The following *user_params.json* file would only add the *$1 vs.* MrBeast videos
268
272
  "keep_duplicates": false,
269
273
  "upload_playlist_ID": "your_playlist_ID",
270
274
  "override_json":false,
275
+ "fancy_mode":true,
271
276
  "verbosity": ["credentials","videos"]
272
277
  }
273
278
  ```
@@ -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=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,,
@@ -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