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