QTube 2.4.0__py3-none-any.whl → 2.5.1__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 +6 -2
- QTube/utils/checks.py +4 -1
- QTube/utils/parsing.py +17 -8
- {QTube-2.4.0.dist-info → qtube-2.5.1.dist-info}/METADATA +46 -18
- qtube-2.5.1.dist-info/RECORD +13 -0
- {QTube-2.4.0.dist-info → qtube-2.5.1.dist-info}/WHEEL +1 -1
- {QTube-2.4.0.dist-info → qtube-2.5.1.dist-info/licenses}/LICENSE.txt +1 -1
- QTube/utils/youtube/captions.py +0 -48
- QTube/utils/youtube/channels.py +0 -110
- QTube/utils/youtube/playlists.py +0 -139
- QTube/utils/youtube/videos.py +0 -649
- QTube-2.4.0.dist-info/RECORD +0 -17
- {QTube-2.4.0.dist-info → qtube-2.5.1.dist-info}/entry_points.txt +0 -0
- {QTube-2.4.0.dist-info → qtube-2.5.1.dist-info}/top_level.txt +0 -0
QTube/scripts/qtube.py
CHANGED
|
@@ -308,6 +308,7 @@ def main():
|
|
|
308
308
|
wanted_channels_upload_playlists.update(partial_dict)
|
|
309
309
|
|
|
310
310
|
## Dictionnary of the latest videos from selected channels
|
|
311
|
+
vid_nb = user_params_dict["video_history_limit"]
|
|
311
312
|
recent_videos = {}
|
|
312
313
|
for ch_name, playlist_Id in wanted_channels_upload_playlists.items():
|
|
313
314
|
latest_partial = QTube.utils.helpers.handle_http_errors(
|
|
@@ -316,6 +317,7 @@ def main():
|
|
|
316
317
|
QTube.utils.youtube.playlists.get_recent_videos,
|
|
317
318
|
youtube,
|
|
318
319
|
playlist_Id,
|
|
320
|
+
vid_nb,
|
|
319
321
|
)
|
|
320
322
|
|
|
321
323
|
if latest_partial == "ignore":
|
|
@@ -367,7 +369,9 @@ def main():
|
|
|
367
369
|
## Additional information retrieving on the videos
|
|
368
370
|
split_videos = QTube.utils.helpers.split_dict(videos, 50)
|
|
369
371
|
|
|
370
|
-
responses = {
|
|
372
|
+
responses = {
|
|
373
|
+
"items": []
|
|
374
|
+
} # Initialize with expected structure to handle empty videos dict case
|
|
371
375
|
for sub_dict in split_videos:
|
|
372
376
|
partial = QTube.utils.helpers.handle_http_errors(
|
|
373
377
|
verb,
|
|
@@ -377,7 +381,7 @@ def main():
|
|
|
377
381
|
sub_dict.keys(),
|
|
378
382
|
)
|
|
379
383
|
|
|
380
|
-
if len(responses) == 0: # first run of the loop
|
|
384
|
+
if len(responses["items"]) == 0: # first run of the loop
|
|
381
385
|
responses.update(partial)
|
|
382
386
|
else:
|
|
383
387
|
vid_dicts = partial["items"]
|
QTube/utils/checks.py
CHANGED
|
@@ -215,7 +215,7 @@ def check_user_params(params_dict: dict) -> bool:
|
|
|
215
215
|
projections_options = ["rectangular", "360"]
|
|
216
216
|
caption_options = [
|
|
217
217
|
"trackKind",
|
|
218
|
-
"
|
|
218
|
+
"language",
|
|
219
219
|
"audioTrackType",
|
|
220
220
|
"isCC",
|
|
221
221
|
"isLarge",
|
|
@@ -351,6 +351,9 @@ def check_user_params(params_dict: dict) -> bool:
|
|
|
351
351
|
and 0 <= params_dict.get("comments_to_views_ratio") <= 1,
|
|
352
352
|
# Paid promotions
|
|
353
353
|
isinstance(params_dict.get("allow_paid_promotions"), bool),
|
|
354
|
+
# Vid number
|
|
355
|
+
isinstance(params_dict.get("video_history_limit"), int)
|
|
356
|
+
and 1 <= params_dict.get("video_history_limit") <= 50,
|
|
354
357
|
]
|
|
355
358
|
|
|
356
359
|
ok = all(checks)
|
QTube/utils/parsing.py
CHANGED
|
@@ -257,6 +257,15 @@ def parse_arguments() -> dict:
|
|
|
257
257
|
help="Minimum ratio of comments to views. Default: 0",
|
|
258
258
|
)
|
|
259
259
|
|
|
260
|
+
parser.add_argument(
|
|
261
|
+
"-vhl",
|
|
262
|
+
"--video_history_limit",
|
|
263
|
+
metavar="",
|
|
264
|
+
type=int,
|
|
265
|
+
default=5,
|
|
266
|
+
help="How far back videos are checked in creators' upload playlist. Default: 5",
|
|
267
|
+
)
|
|
268
|
+
|
|
260
269
|
parser.add_argument(
|
|
261
270
|
"-rf",
|
|
262
271
|
"--run_frequency",
|
|
@@ -274,17 +283,17 @@ def parse_arguments() -> dict:
|
|
|
274
283
|
)
|
|
275
284
|
|
|
276
285
|
parser.add_argument(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
286
|
+
"-app",
|
|
287
|
+
"--allow_paid_promotions",
|
|
288
|
+
action="store_false",
|
|
289
|
+
help="Allow videos containing paid advertising. Default: True",
|
|
281
290
|
)
|
|
282
291
|
|
|
283
292
|
parser.add_argument(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
293
|
+
"-mfk",
|
|
294
|
+
"--only_made_for_kids",
|
|
295
|
+
action="store_true",
|
|
296
|
+
help="Determines whether to only add videos that are made for kids, based on Youtube and FTC guidelines. Default: False",
|
|
288
297
|
)
|
|
289
298
|
|
|
290
299
|
parser.add_argument(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: QTube
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.1
|
|
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) [2026] [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
|
|
@@ -44,15 +44,18 @@ 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
|
|
48
|
-
Requires-Dist:
|
|
49
|
-
Requires-Dist:
|
|
50
|
-
Requires-Dist: isodate
|
|
51
|
-
Requires-Dist: numpy
|
|
52
|
-
Requires-Dist: protobuf
|
|
53
|
-
Requires-Dist: pytube
|
|
54
|
-
Requires-Dist: Requests
|
|
55
|
-
Requires-Dist: setuptools
|
|
47
|
+
Requires-Dist: colorama>=0.4.6
|
|
48
|
+
Requires-Dist: google_api_python_client>=2.119.0
|
|
49
|
+
Requires-Dist: google_auth_oauthlib>=1.0.0
|
|
50
|
+
Requires-Dist: isodate>=0.6.1
|
|
51
|
+
Requires-Dist: numpy>=1.24.3
|
|
52
|
+
Requires-Dist: protobuf>=4.25.1
|
|
53
|
+
Requires-Dist: pytube>=15.0.0
|
|
54
|
+
Requires-Dist: Requests>=2.32.0
|
|
55
|
+
Requires-Dist: setuptools>=70.0.0
|
|
56
|
+
Dynamic: author
|
|
57
|
+
Dynamic: home-page
|
|
58
|
+
Dynamic: license-file
|
|
56
59
|
|
|
57
60
|
<h1 align="center">
|
|
58
61
|
<br>
|
|
@@ -85,12 +88,15 @@ Requires-Dist: setuptools >=70.0.0
|
|
|
85
88
|
</p>
|
|
86
89
|
|
|
87
90
|
## About
|
|
91
|
+
|
|
88
92
|
The reason for the existence of this software is Youtube's seemingly random behavior when it comes to notifying people that a new video has been published (late or missing notifications, useless notification bell, videos not appearing in the subscription tab, ...).
|
|
89
93
|
|
|
90
94
|
With this software, you can set a number of rules that determine which videos are added to a dedicated playlist, so you won't miss any new uploads!
|
|
91
95
|
|
|
92
96
|
## Features
|
|
97
|
+
|
|
93
98
|
Each of these rules is based on putting some kind of constraint on video properties. Currently, the following features are available:
|
|
99
|
+
|
|
94
100
|
* Channel name filtering
|
|
95
101
|
* Title filtering
|
|
96
102
|
* Description filtering
|
|
@@ -109,9 +115,10 @@ Each of these rules is based on putting some kind of constraint on video propert
|
|
|
109
115
|
* Duplicate checking
|
|
110
116
|
|
|
111
117
|
## How to use
|
|
112
|
-
Before using this software, you first need to get a Youtube API key and create a web app to get a client secrets file (that should look like [this](docs/client_secrets_template.json)). This [Corey Schafer video](https://www.youtube.com/watch?v=vQQEaSnQ_bs) goes through the process step by step.
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
Before using this software, you first need to get a Youtube API key and create a web app to get a client secrets file (that should look like [this](docs/client_secrets_template.json)). This [Corey Schafer video](https://www.youtube.com/watch?v=vQQEaSnQ_bs) goes through the process step by step. Rename this file to *client_secrets.json*.
|
|
120
|
+
|
|
121
|
+
Once that's done, download this project or install the package with [PyPI](https://pypi.org/project/QTube/). Modify the [user parameters template](docs/user_params_template.json) file so that it fits your needs (more information on how in the [following table](#user-defined-parameters) and in the [examples section](#examples)). Then, rename the file to *user_params.json* and move it along with the *client_secrets.json* file to the [QTube folder](./QTube/).
|
|
115
122
|
|
|
116
123
|
Verify that you have all of the dependencies installed (see the [requirements](requirements.txt) file or the [TOML](pyproject.toml) file).
|
|
117
124
|
|
|
@@ -122,6 +129,7 @@ I would recommend creating a task to execute the program regularly (like once a
|
|
|
122
129
|
For more versatile uses, you can also use command line arguments with the [qtube.py](QTube/scripts/qtube.py) file. Enable this option by setting the `override_json` parameter to *True* in your JSON user parameters file. Provided command line arguments will then override what is in your JSON user parameters file. This is especially useful to manage different types of videos and put them in dedicated playlists (music playlist, gaming playlist, ect...).
|
|
123
130
|
|
|
124
131
|
### User-defined parameters
|
|
132
|
+
|
|
125
133
|
|Parameter|Optional|Description|Possible values|
|
|
126
134
|
|--|:--:|:--:|:--:|
|
|
127
135
|
|`required_in_channel_name`|Yes|Words that must be in channel names, typically channel names themselves. Videos from channels not containing any of the words of this list in their name will not be added.|Any string|
|
|
@@ -153,6 +161,7 @@ For more versatile uses, you can also use command line arguments with the [qtube
|
|
|
153
161
|
|`comments_threshold`|No|Minimum number of times videos have been commented on.|Positive integer|
|
|
154
162
|
|`likes_to_views_ratio`|No|Minimum likes to views ratio.|Positive float between 0 & 1|
|
|
155
163
|
|`comments_to_views_ratio`|No|Minimum comments to views ratio.|Positive float between 0 & 1|
|
|
164
|
+
|`video_history_limit`|No|How far back videos are checked in creators' upload playlist.|Positive integer between 1 & 50|
|
|
156
165
|
|`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|
|
|
157
166
|
|`keep_shorts`|No|Determines whether to add shorts.|boolean|
|
|
158
167
|
|`allow_paid_promotions`|No|Determines whether to add videos containing paid advertisement.|boolean|
|
|
@@ -166,18 +175,24 @@ For more versatile uses, you can also use command line arguments with the [qtube
|
|
|
166
175
|
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.
|
|
167
176
|
|
|
168
177
|
For further information about each parameter, check the note associated with the [release](https://github.com/Killian42/QTube/releases) they were introduced in.
|
|
178
|
+
|
|
169
179
|
### Requirements
|
|
180
|
+
|
|
170
181
|
See the [requirements](requirements.txt) file or the [TOML](pyproject.toml) file.
|
|
171
182
|
|
|
172
183
|
## Examples
|
|
184
|
+
|
|
173
185
|
This section presents examples of user parameters json files for concrete use-cases.
|
|
186
|
+
|
|
174
187
|
* <a href="#example-1---every-videos-from-subscribed-channels">Every videos from subscribed channels</a>
|
|
175
188
|
* <a href="#example-2---higher-quality-videos">Higher quality videos</a>
|
|
176
|
-
* <a href="#example-3---specific-video-series-from-a-creator">Video series from a creator</a>
|
|
189
|
+
* <a href="#example-3---specific-video-series-from-a-creator">Video series from a creator</a>
|
|
177
190
|
|
|
178
191
|
### Example 1 - Every videos from subscribed channels
|
|
192
|
+
|
|
179
193
|
The following *user_params.json* file would add every new videos from channels you are subcribed to.
|
|
180
|
-
|
|
194
|
+
|
|
195
|
+
```python
|
|
181
196
|
{
|
|
182
197
|
"required_in_channel_name": null,
|
|
183
198
|
"banned_in_channel_name": null,
|
|
@@ -208,6 +223,7 @@ The following *user_params.json* file would add every new videos from channels y
|
|
|
208
223
|
"comments_threshold": 0,
|
|
209
224
|
"likes_to_views_ratio": 0,
|
|
210
225
|
"comments_to_views_ratio": 0,
|
|
226
|
+
"video_history_limit": 5,
|
|
211
227
|
"run_frequency":"daily",
|
|
212
228
|
"keep_shorts": true,
|
|
213
229
|
"allow_paid_promotions": true,
|
|
@@ -219,9 +235,12 @@ The following *user_params.json* file would add every new videos from channels y
|
|
|
219
235
|
"verbosity": ["credentials","videos"]
|
|
220
236
|
}
|
|
221
237
|
```
|
|
238
|
+
|
|
222
239
|
### Example 2 - Higher quality videos
|
|
240
|
+
|
|
223
241
|
The following *user_params.json* file would only add videos with good quality.
|
|
224
|
-
|
|
242
|
+
|
|
243
|
+
```python
|
|
225
244
|
{
|
|
226
245
|
"required_in_channel_name": null,
|
|
227
246
|
"banned_in_channel_name": null,
|
|
@@ -252,6 +271,7 @@ The following *user_params.json* file would only add videos with good quality.
|
|
|
252
271
|
"comments_threshold": 0,
|
|
253
272
|
"likes_to_views_ratio": 0,
|
|
254
273
|
"comments_to_views_ratio": 0,
|
|
274
|
+
"video_history_limit": 5,
|
|
255
275
|
"run_frequency":"daily",
|
|
256
276
|
"keep_shorts": true,
|
|
257
277
|
"allow_paid_promotions": true,
|
|
@@ -263,9 +283,12 @@ The following *user_params.json* file would only add videos with good quality.
|
|
|
263
283
|
"verbosity": ["credentials","videos"]
|
|
264
284
|
}
|
|
265
285
|
```
|
|
286
|
+
|
|
266
287
|
### Example 3 - Specific video series from a creator
|
|
288
|
+
|
|
267
289
|
The following *user_params.json* file would only add the *$1 vs.* MrBeast videos.
|
|
268
|
-
|
|
290
|
+
|
|
291
|
+
```python
|
|
269
292
|
{
|
|
270
293
|
"required_in_channel_name": ["MrBeast"],
|
|
271
294
|
"banned_in_channel_name": null,
|
|
@@ -296,6 +319,7 @@ The following *user_params.json* file would only add the *$1 vs.* MrBeast videos
|
|
|
296
319
|
"comments_threshold": 0,
|
|
297
320
|
"likes_to_views_ratio": 0,
|
|
298
321
|
"comments_to_views_ratio": 0,
|
|
322
|
+
"video_history_limit": 5,
|
|
299
323
|
"run_frequency":"daily",
|
|
300
324
|
"keep_shorts": false,
|
|
301
325
|
"allow_paid_promotions": true,
|
|
@@ -309,13 +333,17 @@ The following *user_params.json* file would only add the *$1 vs.* MrBeast videos
|
|
|
309
333
|
```
|
|
310
334
|
|
|
311
335
|
## FAQ
|
|
336
|
+
|
|
312
337
|
There are none yet. But don't hesitate to ask by sending me an [email](mailto:killian.lebreton35@gmail.com).
|
|
313
338
|
|
|
314
339
|
## Contact
|
|
340
|
+
|
|
315
341
|
You can reach me by [email](mailto:killian.lebreton35@gmail.com). Please put *QTube* in the subject line.
|
|
316
342
|
|
|
317
343
|
## Acknowledgments
|
|
344
|
+
|
|
318
345
|
Big thanks [Corey Schafer](https://github.com/CoreyMSchafer) for his great tutorials, as well as for providing the OAuth snippets used in this software.
|
|
319
346
|
|
|
320
347
|
## License
|
|
348
|
+
|
|
321
349
|
This project is licensed under the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,13 @@
|
|
|
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=bJuD2BJ51Gv4cx26d-dN3Mbg4t0SXMmatINWoZYavEM,35290
|
|
4
|
+
QTube/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
QTube/utils/checks.py,sha256=gqkZtTv4tk8i9vgzy68a1IC7jk575BC2yUEaHpX7zu4,13309
|
|
6
|
+
QTube/utils/helpers.py,sha256=fl1_fePwy7ot-KRFB9Ieq7fMFtD8Q4TriK8cW17qPy0,9187
|
|
7
|
+
QTube/utils/parsing.py,sha256=H1uVtevbDF-6JWy42--pDvkBBHzAvxBgujIErQk54AI,11057
|
|
8
|
+
qtube-2.5.1.dist-info/licenses/LICENSE.txt,sha256=Z9Z3S-Ah5KeUZAbXRObR5OLIws8vsBwdSib4jX0RE7U,1098
|
|
9
|
+
qtube-2.5.1.dist-info/METADATA,sha256=GqByZxpWCNy9P44KbMKUGrtM53lM54Q1oiGmkNyMbmc,18190
|
|
10
|
+
qtube-2.5.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
qtube-2.5.1.dist-info/entry_points.txt,sha256=Q3SLDyRahuzrNnHC3UOkMH5gM_Um3eCLiLG4vSTPjqE,51
|
|
12
|
+
qtube-2.5.1.dist-info/top_level.txt,sha256=z6oXrT8BiTTZuygjsbfe-NE4rmkHlDK8euAL9m6BT4A,6
|
|
13
|
+
qtube-2.5.1.dist-info/RECORD,,
|
QTube/utils/youtube/captions.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
def make_caption_requests(youtube, video_IDs: list[str]) -> dict[dict]:
|
|
2
|
-
"""Retrieves API caption responses of a list of YT videos.
|
|
3
|
-
|
|
4
|
-
Args:
|
|
5
|
-
youtube (Resource): YT API resource.
|
|
6
|
-
video_IDs (list[str]): List of video IDs.
|
|
7
|
-
|
|
8
|
-
Returns:
|
|
9
|
-
responses_dict (dict[dict]): Dictionary with video IDs as keys and YT API caption responses as values.
|
|
10
|
-
"""
|
|
11
|
-
responses_dict = {
|
|
12
|
-
video_ID: youtube.captions()
|
|
13
|
-
.list(part="snippet", videoId=video_ID)
|
|
14
|
-
.execute(num_retries=5)
|
|
15
|
-
for video_ID in video_IDs
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return responses_dict
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def get_captions(
|
|
22
|
-
youtube=None,
|
|
23
|
-
response: dict = None,
|
|
24
|
-
video_IDs: list[str] = None,
|
|
25
|
-
use_API: bool = False,
|
|
26
|
-
) -> dict[dict]:
|
|
27
|
-
"""Retrieves the captions of YT videos.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
youtube (Resource): YT API resource.
|
|
31
|
-
response (dict[dict]): YT API response from the make_caption_request function.
|
|
32
|
-
video_IDs (list[str]): List of video IDs.
|
|
33
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
captions_dict (dict[dict]): Dictionary with video IDs as keys and caption dictionaries as values.
|
|
37
|
-
"""
|
|
38
|
-
if use_API:
|
|
39
|
-
response = make_caption_requests(youtube, video_IDs)
|
|
40
|
-
|
|
41
|
-
captions_dict = {}
|
|
42
|
-
for video_ID, vid_resp in response.items():
|
|
43
|
-
caption_dict = {
|
|
44
|
-
caption["id"]: caption["snippet"] for caption in vid_resp.get("items", [])
|
|
45
|
-
}
|
|
46
|
-
captions_dict.update({video_ID: caption_dict})
|
|
47
|
-
|
|
48
|
-
return captions_dict
|
QTube/utils/youtube/channels.py
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
def get_subscriptions(youtube, next_page_token=None) -> dict:
|
|
2
|
-
"""Retrieves the subscriptions of the logged user.
|
|
3
|
-
|
|
4
|
-
Args:
|
|
5
|
-
youtube (Resource): YT API resource.
|
|
6
|
-
next_page_token (str): Token of the subscription page (optional).
|
|
7
|
-
|
|
8
|
-
Returns:
|
|
9
|
-
channels (dict): Dictionary of channel names (keys) and channel IDs (values).
|
|
10
|
-
"""
|
|
11
|
-
channels = {}
|
|
12
|
-
|
|
13
|
-
while True:
|
|
14
|
-
response = (
|
|
15
|
-
youtube.subscriptions()
|
|
16
|
-
.list(
|
|
17
|
-
part="snippet",
|
|
18
|
-
mine=True,
|
|
19
|
-
maxResults=50,
|
|
20
|
-
order="alphabetical",
|
|
21
|
-
pageToken=next_page_token,
|
|
22
|
-
)
|
|
23
|
-
.execute(num_retries=5)
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
for item in response.get("items", []):
|
|
27
|
-
title = item["snippet"]["title"]
|
|
28
|
-
channel_id = item["snippet"]["resourceId"]["channelId"]
|
|
29
|
-
channels[title] = channel_id
|
|
30
|
-
|
|
31
|
-
next_page_token = response.get("nextPageToken")
|
|
32
|
-
|
|
33
|
-
if not next_page_token:
|
|
34
|
-
break
|
|
35
|
-
|
|
36
|
-
return channels
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_channel_info(youtube, handle: str) -> dict:
|
|
40
|
-
"""Retrieves basic information about a YT channel.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
youtube (Resource): YT API resource.
|
|
44
|
-
handle (str): Handle of the YT channel.
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
response (dict): Dictionary containing basic information on the requested YT channel.
|
|
48
|
-
"""
|
|
49
|
-
channel = {}
|
|
50
|
-
|
|
51
|
-
response = (
|
|
52
|
-
youtube.channels().list(part="snippet", forHandle=handle).execute(num_retries=5)
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
if "items" in response.keys():
|
|
56
|
-
title = response["items"][0]["snippet"]["title"]
|
|
57
|
-
channel_id = response["items"][0]["id"]
|
|
58
|
-
channel[title] = channel_id
|
|
59
|
-
else:
|
|
60
|
-
print(
|
|
61
|
-
f"Could not find a YT channel associated with the following handle: {handle}."
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
return channel
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def get_uploads_playlists(youtube, channel_IDs: list[str]) -> list[str]:
|
|
68
|
-
"""Retrieves the upload playlists of YT channels.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
youtube (Resource): YT API ressource.
|
|
72
|
-
channel_IDs (list[str]): Channel IDs of YT channels.
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
upload_pl_ids (list[str]): IDs of the uploads playlist of the YT channels.
|
|
76
|
-
"""
|
|
77
|
-
channel_IDs_str = ",".join(channel_IDs)
|
|
78
|
-
response = (
|
|
79
|
-
youtube.channels()
|
|
80
|
-
.list(part="contentDetails", id=channel_IDs_str)
|
|
81
|
-
.execute(num_retries=5)
|
|
82
|
-
)
|
|
83
|
-
# Create a dictionary to store the mapping between channel IDs and upload playlist IDs
|
|
84
|
-
channel_to_upload_map = {
|
|
85
|
-
item["id"]: item["contentDetails"]["relatedPlaylists"]["uploads"]
|
|
86
|
-
for item in response.get("items", [])
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
# Generate the resulting list in the same order as the input channel IDs
|
|
90
|
-
upload_pl_ids = [channel_to_upload_map[channel_ID] for channel_ID in channel_IDs]
|
|
91
|
-
|
|
92
|
-
return upload_pl_ids
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def get_user_info(youtube) -> dict:
|
|
96
|
-
"""Retrieves information about the logged-in user channel.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
youtube (Resource): YT API resource.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
response (dict): Dictionary containing information on the logged-in user channel.
|
|
103
|
-
"""
|
|
104
|
-
response = (
|
|
105
|
-
youtube.channels()
|
|
106
|
-
.list(part="snippet,contentDetails,statistics", mine=True)
|
|
107
|
-
.execute(num_retries=5)
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
return response
|
QTube/utils/youtube/playlists.py
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import datetime as dt
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def get_recent_videos(youtube, playlist_ID: str) -> dict:
|
|
5
|
-
"""Retrieves the last 5 videos of a YT playlist.
|
|
6
|
-
|
|
7
|
-
Args:
|
|
8
|
-
youtube (Resource): YT API resource.
|
|
9
|
-
playlist_ID (str): ID of the playlist.
|
|
10
|
-
|
|
11
|
-
Returns:
|
|
12
|
-
recent_vids (dict): Dictionary containing the ID (keys) and upload date (values) of the last 5 videos in the playlist.
|
|
13
|
-
"""
|
|
14
|
-
response = (
|
|
15
|
-
youtube.playlistItems()
|
|
16
|
-
.list(part="contentDetails", playlistId=playlist_ID, maxResults=5)
|
|
17
|
-
.execute(num_retries=5)
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
recent_vids = {
|
|
21
|
-
item["contentDetails"]["videoId"]: {
|
|
22
|
-
"upload datetime": dt.datetime.fromisoformat(
|
|
23
|
-
item["contentDetails"]["videoPublishedAt"]
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
for item in response.get("items", [])
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return recent_vids
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def get_playlist_content(youtube, playlist_ID: str) -> list[str]:
|
|
33
|
-
"""Retrieves the IDs of videos saved in a YT playlist.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
youtube (Resource): YT API resource.
|
|
37
|
-
playlist_ID (str): ID of the playlist.
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
videos_IDs (list[str]): List containing the IDs of the videos saved in the playlist.
|
|
41
|
-
"""
|
|
42
|
-
next_page_token = None
|
|
43
|
-
videos_IDs = []
|
|
44
|
-
while True:
|
|
45
|
-
response = (
|
|
46
|
-
youtube.playlistItems()
|
|
47
|
-
.list(
|
|
48
|
-
part="contentDetails",
|
|
49
|
-
playlistId=playlist_ID,
|
|
50
|
-
maxResults=50,
|
|
51
|
-
pageToken=next_page_token,
|
|
52
|
-
)
|
|
53
|
-
.execute(num_retries=5)
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
temp_videos_IDs = [
|
|
57
|
-
item["contentDetails"]["videoId"] for item in response.get("items")
|
|
58
|
-
]
|
|
59
|
-
videos_IDs.extend(temp_videos_IDs)
|
|
60
|
-
|
|
61
|
-
next_page_token = response.get("nextPageToken")
|
|
62
|
-
|
|
63
|
-
if not next_page_token:
|
|
64
|
-
break
|
|
65
|
-
|
|
66
|
-
return videos_IDs
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def get_playlists_titles(youtube=None, playlist_IDs: list[str] = None) -> list[str]:
|
|
70
|
-
"""Retrieves the titles of a list of YT playlists.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
youtube (Resource): YT API resource.
|
|
74
|
-
playlists_IDs (list[str]): List of playlist IDs.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
titles (list[str]): List of YT playlist titles.
|
|
78
|
-
"""
|
|
79
|
-
playlist_IDs_str = ",".join(playlist_IDs)
|
|
80
|
-
response = (
|
|
81
|
-
youtube.playlists()
|
|
82
|
-
.list(part="snippet", id=playlist_IDs_str)
|
|
83
|
-
.execute(num_retries=5)
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
titles = [playlist["snippet"]["title"] for playlist in response["items"]]
|
|
87
|
-
|
|
88
|
-
return titles
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def get_playlists_video_counts(
|
|
92
|
-
youtube=None, playlist_IDs: list[str] = None
|
|
93
|
-
) -> list[int]:
|
|
94
|
-
"""Retrieves the number of videos of a list of YT playlists.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
youtube (Resource): YT API resource.
|
|
98
|
-
playlists_IDs (list[str]): List of playlist IDs.
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
counts (list[int]): List of YT playlist video counts.
|
|
102
|
-
"""
|
|
103
|
-
playlist_IDs_str = ",".join(playlist_IDs)
|
|
104
|
-
response = (
|
|
105
|
-
youtube.playlists()
|
|
106
|
-
.list(part="contentDetails", id=playlist_IDs_str)
|
|
107
|
-
.execute(num_retries=5)
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
counts = [playlist["contentDetails"]["itemCount"] for playlist in response["items"]]
|
|
111
|
-
|
|
112
|
-
return counts
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def add_to_playlist(youtube, playlist_ID: str, video_ID: str) -> None:
|
|
116
|
-
"""Adds a YT video to the YT playlist.
|
|
117
|
-
|
|
118
|
-
Args:
|
|
119
|
-
youtube (Resource): YT API resource.
|
|
120
|
-
playlist_ID (str): Playlist ID.
|
|
121
|
-
video_ID (str): Video ID.
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
None
|
|
125
|
-
"""
|
|
126
|
-
response = (
|
|
127
|
-
youtube.playlistItems()
|
|
128
|
-
.insert(
|
|
129
|
-
part="snippet",
|
|
130
|
-
body={
|
|
131
|
-
"snippet": {
|
|
132
|
-
"playlistId": playlist_ID,
|
|
133
|
-
"resourceId": {"kind": "youtube#video", "videoId": video_ID},
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
)
|
|
137
|
-
.execute(num_retries=5)
|
|
138
|
-
)
|
|
139
|
-
return
|
QTube/utils/youtube/videos.py
DELETED
|
@@ -1,649 +0,0 @@
|
|
|
1
|
-
import isodate
|
|
2
|
-
|
|
3
|
-
from pytube import YouTube
|
|
4
|
-
|
|
5
|
-
from QTube.utils import helpers, checks
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def make_video_requests(youtube, video_IDs: list[str]) -> dict:
|
|
9
|
-
"""Retrieves information on a list of YT videos.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
youtube (Resource): YT API resource.
|
|
13
|
-
video_IDs (list[str]): List of video IDs.
|
|
14
|
-
|
|
15
|
-
Returns:
|
|
16
|
-
response (dict[dict]): YT API response.
|
|
17
|
-
"""
|
|
18
|
-
video_IDs_str = ",".join(video_IDs)
|
|
19
|
-
response = (
|
|
20
|
-
youtube.videos()
|
|
21
|
-
.list(
|
|
22
|
-
part="snippet,contentDetails,statistics,paidProductPlacementDetails,status",
|
|
23
|
-
id=video_IDs_str,
|
|
24
|
-
)
|
|
25
|
-
.execute(num_retries=5)
|
|
26
|
-
)
|
|
27
|
-
return response
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def get_titles(
|
|
31
|
-
youtube=None,
|
|
32
|
-
response: dict = None,
|
|
33
|
-
video_IDs: list[str] = None,
|
|
34
|
-
use_API: bool = False,
|
|
35
|
-
) -> list[str]:
|
|
36
|
-
"""Retrieves the titles of a list of YT videos.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
youtube (Resource): YT API resource.
|
|
40
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
41
|
-
video_IDs (list[str]): List of video IDs.
|
|
42
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
titles (list[str]): List of YT videos titles.
|
|
46
|
-
"""
|
|
47
|
-
if use_API:
|
|
48
|
-
video_IDs_str = ",".join(video_IDs)
|
|
49
|
-
response = (
|
|
50
|
-
youtube.videos()
|
|
51
|
-
.list(part="snippet", id=video_IDs_str)
|
|
52
|
-
.execute(num_retries=5)
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
titles = [vid["snippet"]["title"] for vid in response["items"]]
|
|
56
|
-
|
|
57
|
-
return titles
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def get_tags(
|
|
61
|
-
youtube=None,
|
|
62
|
-
response: dict = None,
|
|
63
|
-
video_IDs: list[str] = None,
|
|
64
|
-
use_API: bool = False,
|
|
65
|
-
) -> list[list[str] | None]:
|
|
66
|
-
"""Retrieves the tags of YT videos.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
youtube (Resource): YT API resource.
|
|
70
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
71
|
-
video_IDs (list[str]): List of video IDs.
|
|
72
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
tags (list[list[str]|None]): List of YT videos tags, or None if there are no tags for this video.
|
|
76
|
-
"""
|
|
77
|
-
if use_API:
|
|
78
|
-
video_IDs_str = ",".join(video_IDs)
|
|
79
|
-
response = (
|
|
80
|
-
youtube.videos()
|
|
81
|
-
.list(part="snippet", id=video_IDs_str)
|
|
82
|
-
.execute(num_retries=5)
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
tags = [
|
|
86
|
-
vid["snippet"]["tags"] if "tags" in vid["snippet"] else None
|
|
87
|
-
for vid in response.get("items", [])
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
return tags
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def get_descriptions(
|
|
94
|
-
youtube=None,
|
|
95
|
-
response: dict = None,
|
|
96
|
-
video_IDs: list[str] = None,
|
|
97
|
-
use_API: bool = False,
|
|
98
|
-
) -> list[str]:
|
|
99
|
-
"""Retrieves the descriptions of YT videos.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
youtube (Resource): YT API resource.
|
|
103
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
104
|
-
video_IDs (list[str]): List of video IDs.
|
|
105
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
description (list[str]): YT videos descriptions.
|
|
109
|
-
"""
|
|
110
|
-
if use_API:
|
|
111
|
-
video_IDs_str = ",".join(video_IDs) # Join the video IDs with commas
|
|
112
|
-
response = (
|
|
113
|
-
youtube.videos()
|
|
114
|
-
.list(part="snippet", id=video_IDs_str)
|
|
115
|
-
.execute(num_retries=5)
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
descriptions = [vid["snippet"]["description"] for vid in response.get("items", [])]
|
|
119
|
-
return descriptions
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def get_durations(
|
|
123
|
-
youtube=None,
|
|
124
|
-
response: dict = None,
|
|
125
|
-
video_IDs: list[str] = None,
|
|
126
|
-
use_API: bool = False,
|
|
127
|
-
) -> list[float]:
|
|
128
|
-
"""Retrieves the duration of YT videos.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
youtube (Resource): YT API resource.
|
|
132
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
133
|
-
video_IDs (list[str]): List of video IDs.
|
|
134
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
durations (list[float]): List of YT videos durations in seconds.
|
|
138
|
-
"""
|
|
139
|
-
if use_API:
|
|
140
|
-
video_IDs_str = ",".join(video_IDs)
|
|
141
|
-
response = (
|
|
142
|
-
youtube.videos()
|
|
143
|
-
.list(part="contentDetails", id=video_IDs_str)
|
|
144
|
-
.execute(num_retries=5)
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
durations_iso = [
|
|
148
|
-
vid["contentDetails"].get("duration", "PT0M3.14159265S")
|
|
149
|
-
for vid in response["items"]
|
|
150
|
-
] # Set the duration of videos without duration info to pi seconds to identify them without breaking the iso conversion
|
|
151
|
-
durations = [isodate.parse_duration(d).total_seconds() for d in durations_iso]
|
|
152
|
-
return durations
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def get_languages(
|
|
156
|
-
youtube=None,
|
|
157
|
-
response: dict = None,
|
|
158
|
-
video_IDs: list[str] = None,
|
|
159
|
-
use_API: bool = False,
|
|
160
|
-
) -> list[str]:
|
|
161
|
-
"""Retrieves the original language of YT videos.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
youtube (Resource): YT API resource.
|
|
165
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
166
|
-
video_IDs (list[str]): List of video IDs.
|
|
167
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
languages (list[str]): List of YT videos languages.
|
|
171
|
-
"""
|
|
172
|
-
if use_API:
|
|
173
|
-
video_IDs_str = ",".join(video_IDs)
|
|
174
|
-
response = (
|
|
175
|
-
youtube.videos()
|
|
176
|
-
.list(part="snippet", id=video_IDs_str)
|
|
177
|
-
.execute(num_retries=5)
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
languages = [
|
|
181
|
-
(
|
|
182
|
-
vid["snippet"]["defaultAudioLanguage"]
|
|
183
|
-
if "defaultAudioLanguage" in vid["snippet"]
|
|
184
|
-
else (
|
|
185
|
-
vid["snippet"]["defaultLanguage"]
|
|
186
|
-
if "defaultLanguage" in vid["snippet"]
|
|
187
|
-
else "unknown"
|
|
188
|
-
)
|
|
189
|
-
).split("-")[
|
|
190
|
-
0
|
|
191
|
-
] # strips regional specifiers
|
|
192
|
-
for vid in response["items"]
|
|
193
|
-
]
|
|
194
|
-
|
|
195
|
-
return languages
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def get_dimensions(
|
|
199
|
-
youtube=None,
|
|
200
|
-
response: dict = None,
|
|
201
|
-
video_IDs: list[str] = None,
|
|
202
|
-
use_API: bool = False,
|
|
203
|
-
) -> list[str]:
|
|
204
|
-
"""Retrieves the dimension of YT videos (2D or 3D).
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
youtube (Resource): YT API resource.
|
|
208
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
209
|
-
video_IDs (list[str]): List of video IDs.
|
|
210
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
dimensions (list[str]): List of YT videos dimensions.
|
|
214
|
-
"""
|
|
215
|
-
if use_API:
|
|
216
|
-
video_IDs_str = ",".join(video_IDs)
|
|
217
|
-
response = (
|
|
218
|
-
youtube.videos()
|
|
219
|
-
.list(part="contentDetails", id=video_IDs_str)
|
|
220
|
-
.execute(num_retries=5)
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
dimensions = [vid["contentDetails"]["dimension"] for vid in response["items"]]
|
|
224
|
-
return dimensions
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def get_definitions(
|
|
228
|
-
youtube=None,
|
|
229
|
-
response: dict = None,
|
|
230
|
-
video_IDs: list[str] = None,
|
|
231
|
-
use_API: bool = False,
|
|
232
|
-
) -> list[str]:
|
|
233
|
-
"""Retrieves the definition (sd or hd) of YT videos.
|
|
234
|
-
|
|
235
|
-
Args:
|
|
236
|
-
youtube (Resource): YT API resource.
|
|
237
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
238
|
-
video_IDs (list[str]): List of video IDs.
|
|
239
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
240
|
-
|
|
241
|
-
Returns:
|
|
242
|
-
definitions (list[str]): List of YT videos definitions.
|
|
243
|
-
"""
|
|
244
|
-
if use_API:
|
|
245
|
-
video_IDs_str = ",".join(video_IDs)
|
|
246
|
-
response = (
|
|
247
|
-
youtube.videos()
|
|
248
|
-
.list(part="contentDetails", id=video_IDs_str)
|
|
249
|
-
.execute(num_retries=5)
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
definitions = [vid["contentDetails"]["definition"] for vid in response["items"]]
|
|
253
|
-
return definitions
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def get_resolutions(video_IDs: list[str] = None) -> dict[str, list[int]]:
|
|
257
|
-
"""Retrieves the resolutions of YT videos.
|
|
258
|
-
This function does not rely on the YT API but on a third party
|
|
259
|
-
package (pytube), so it takes longer to run.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
video_IDs (list[str]): List of video IDs.
|
|
263
|
-
|
|
264
|
-
Returns:
|
|
265
|
-
resolutions (dict[str, list[int]]): Dictionary mapping video IDs to the resolutions.
|
|
266
|
-
"""
|
|
267
|
-
base_url = "http://youtube.com/watch?v"
|
|
268
|
-
resolutions = {}
|
|
269
|
-
|
|
270
|
-
for vid_ID in video_IDs:
|
|
271
|
-
url = f"{base_url}={vid_ID}"
|
|
272
|
-
|
|
273
|
-
try:
|
|
274
|
-
yt = YouTube(url)
|
|
275
|
-
vid_resolutions = list(
|
|
276
|
-
{
|
|
277
|
-
int(stream.resolution.split("p")[0])
|
|
278
|
-
for stream in yt.streams.filter(type="video")
|
|
279
|
-
}
|
|
280
|
-
)
|
|
281
|
-
resolutions[vid_ID] = vid_resolutions
|
|
282
|
-
except Exception as e:
|
|
283
|
-
print(f"Error processing video {vid_ID}: {e}")
|
|
284
|
-
|
|
285
|
-
return resolutions
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def get_framerates(video_IDs: list[str] = None) -> dict[str, list[int]]:
|
|
289
|
-
"""Retrieves the framerates of YT videos.
|
|
290
|
-
This function does not rely on the YT API but on a third party
|
|
291
|
-
package (pytube), so it takes longer to run.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
video_IDs (list[str]): List of video IDs.
|
|
295
|
-
|
|
296
|
-
Returns:
|
|
297
|
-
framerates (dict[str, list[int]]): Dictionary mapping video IDs to the framerates.
|
|
298
|
-
"""
|
|
299
|
-
base_url = "http://youtube.com/watch?v"
|
|
300
|
-
framerates = {}
|
|
301
|
-
|
|
302
|
-
for vid_ID in video_IDs:
|
|
303
|
-
url = f"{base_url}={vid_ID}"
|
|
304
|
-
|
|
305
|
-
try:
|
|
306
|
-
yt = YouTube(url)
|
|
307
|
-
vid_framerates = list(
|
|
308
|
-
{stream.fps for stream in yt.streams.filter(type="video")}
|
|
309
|
-
)
|
|
310
|
-
framerates[vid_ID] = vid_framerates
|
|
311
|
-
except Exception as e:
|
|
312
|
-
print(f"Error processing video {vid_ID}: {e}")
|
|
313
|
-
|
|
314
|
-
return framerates
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def get_projections(
|
|
318
|
-
youtube=None,
|
|
319
|
-
response: dict = None,
|
|
320
|
-
video_IDs: list[str] = None,
|
|
321
|
-
use_API: bool = False,
|
|
322
|
-
) -> list[str]:
|
|
323
|
-
"""Retrieves the projection (360 or rectangular) of YT videos.
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
youtube (Resource): YT API resource.
|
|
327
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
328
|
-
video_IDs (list[str]): List of video IDs.
|
|
329
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
330
|
-
|
|
331
|
-
Returns:
|
|
332
|
-
projections (list[str]): List of YT videos projections.
|
|
333
|
-
"""
|
|
334
|
-
if use_API:
|
|
335
|
-
video_IDs_str = ",".join(video_IDs)
|
|
336
|
-
response = (
|
|
337
|
-
youtube.videos()
|
|
338
|
-
.list(part="contentDetails", id=video_IDs_str)
|
|
339
|
-
.execute(num_retries=5)
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
projections = [vid["contentDetails"]["projection"] for vid in response["items"]]
|
|
343
|
-
return projections
|
|
344
|
-
|
|
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
|
-
|
|
498
|
-
def has_captions(
|
|
499
|
-
youtube=None,
|
|
500
|
-
response: dict = None,
|
|
501
|
-
video_IDs: list[str] = None,
|
|
502
|
-
use_API: bool = False,
|
|
503
|
-
) -> list[bool]:
|
|
504
|
-
"""Determines if YT videos have captions.
|
|
505
|
-
|
|
506
|
-
Args:
|
|
507
|
-
youtube (Resource): YT API resource.
|
|
508
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
509
|
-
video_IDs (list[str]): List of video IDs.
|
|
510
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
511
|
-
|
|
512
|
-
Returns:
|
|
513
|
-
captions (list[bool]): True if the video has captions, False otherwise.
|
|
514
|
-
"""
|
|
515
|
-
if use_API:
|
|
516
|
-
video_IDs_str = ",".join(video_IDs)
|
|
517
|
-
response = (
|
|
518
|
-
youtube.videos()
|
|
519
|
-
.list(part="contentDetails", id=video_IDs_str)
|
|
520
|
-
.execute(num_retries=5)
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
captions = [vid["contentDetails"]["caption"] for vid in response["items"]]
|
|
524
|
-
return captions
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
def is_short(
|
|
528
|
-
youtube=None,
|
|
529
|
-
response: dict = None,
|
|
530
|
-
video_IDs: list[str] = None,
|
|
531
|
-
use_API: bool = False,
|
|
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.
|
|
534
|
-
|
|
535
|
-
Args:
|
|
536
|
-
youtube (Resource): YT API resource.
|
|
537
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
538
|
-
video_IDs (list[str]): List of video IDs.
|
|
539
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
540
|
-
|
|
541
|
-
Returns:
|
|
542
|
-
is_a_short (list[bool]): True if the video is shorter than 181 seconds and there is no URL redirection, False otherwise.
|
|
543
|
-
"""
|
|
544
|
-
durations = get_durations(youtube, response, video_IDs, use_API=use_API)
|
|
545
|
-
|
|
546
|
-
is_short_vid = [
|
|
547
|
-
True if length <= 181 else False for length in durations
|
|
548
|
-
] # Shorts cannot last over 3 minutes.
|
|
549
|
-
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
def is_live(
|
|
560
|
-
youtube=None,
|
|
561
|
-
response: dict = None,
|
|
562
|
-
video_IDs: list[str] = None,
|
|
563
|
-
use_API: bool = False,
|
|
564
|
-
) -> list[str]:
|
|
565
|
-
"""Retrieves the live status of YT videos.
|
|
566
|
-
|
|
567
|
-
Args:
|
|
568
|
-
youtube (Resource): YT API resource.
|
|
569
|
-
response (dict[dict]): YT API response from the make_video_request function.
|
|
570
|
-
video_IDs (list[str]): List of video IDs.
|
|
571
|
-
use_API (bool): Determines if a new API request is made or if the response dictionary is used.
|
|
572
|
-
|
|
573
|
-
Returns:
|
|
574
|
-
live_statuses (list[str]): live if the video is live, upcoming if it is a premiere and none otherwise.
|
|
575
|
-
"""
|
|
576
|
-
if use_API:
|
|
577
|
-
video_IDs_str = ",".join(video_IDs)
|
|
578
|
-
response = (
|
|
579
|
-
youtube.videos()
|
|
580
|
-
.list(part="snippet", id=video_IDs_str)
|
|
581
|
-
.execute(num_retries=5)
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
live_statuses = [
|
|
585
|
-
vid["snippet"]["liveBroadcastContent"] for vid in response["items"]
|
|
586
|
-
]
|
|
587
|
-
|
|
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"]]
|
QTube-2.4.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=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,,
|
|
File without changes
|
|
File without changes
|