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 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
- "languages",
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
- "-app",
278
- "--allow_paid_promotions",
279
- action="store_false",
280
- help="Allow videos containing paid advertising. Default: True",
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
- "-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",
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
1
+ Metadata-Version: 2.4
2
2
  Name: QTube
3
- Version: 2.4.0
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) [2025] [Killian Lebreton]
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 >=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
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
- Once that's done, download this project or install the package with [PyPI](https://pypi.org/project/QTube/). Then, copy the [user parameters template](docs/user_params_template.json) file to the directory where the project is saved and rename it to *user_params.json*. Modify it so that it fits your needs (more information on how in the [following table](#user-defined-parameters) and in the [examples section](#examples)).
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) [2025] [Killian Lebreton]
3
+ Copyright (c) [2026] [Killian Lebreton]
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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
@@ -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
@@ -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
@@ -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"]]
@@ -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,,