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 +184 -56
- QTube/utils/checks.py +13 -7
- QTube/utils/helpers.py +42 -6
- QTube/utils/parsing.py +16 -5
- QTube/utils/youtube/captions.py +3 -1
- QTube/utils/youtube/channels.py +10 -4
- QTube/utils/youtube/playlists.py +30 -4
- QTube/utils/youtube/videos.py +43 -11
- {QTube-2.1.1.dist-info → QTube-2.2.0.dist-info}/METADATA +6 -1
- QTube-2.2.0.dist-info/RECORD +17 -0
- QTube-2.1.1.dist-info/RECORD +0 -17
- {QTube-2.1.1.dist-info → QTube-2.2.0.dist-info}/LICENSE.txt +0 -0
- {QTube-2.1.1.dist-info → QTube-2.2.0.dist-info}/WHEEL +0 -0
- {QTube-2.1.1.dist-info → QTube-2.2.0.dist-info}/entry_points.txt +0 -0
- {QTube-2.1.1.dist-info → QTube-2.2.0.dist-info}/top_level.txt +0 -0
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(
|
|
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
|
-
|
|
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...",
|
|
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",
|
|
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...",
|
|
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",
|
|
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...",
|
|
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",
|
|
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,
|
|
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,
|
|
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.",
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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"
|
|
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 =
|
|
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
|
|
63
|
-
"""
|
|
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
|
-
|
|
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(
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
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
|
|
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("'", '"')
|
QTube/utils/youtube/captions.py
CHANGED
|
@@ -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()
|
|
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
|
|
QTube/utils/youtube/channels.py
CHANGED
|
@@ -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 =
|
|
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()
|
|
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
|
QTube/utils/youtube/playlists.py
CHANGED
|
@@ -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 =
|
|
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
|
QTube/utils/youtube/videos.py
CHANGED
|
@@ -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()
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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()
|
|
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 =
|
|
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()
|
|
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()
|
|
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()
|
|
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()
|
|
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 =
|
|
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.
|
|
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,,
|
QTube-2.1.1.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|