QTube 2.5.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
@@ -369,7 +369,9 @@ def main():
369
369
  ## Additional information retrieving on the videos
370
370
  split_videos = QTube.utils.helpers.split_dict(videos, 50)
371
371
 
372
- responses = {}
372
+ responses = {
373
+ "items": []
374
+ } # Initialize with expected structure to handle empty videos dict case
373
375
  for sub_dict in split_videos:
374
376
  partial = QTube.utils.helpers.handle_http_errors(
375
377
  verb,
@@ -379,7 +381,7 @@ def main():
379
381
  sub_dict.keys(),
380
382
  )
381
383
 
382
- if len(responses) == 0: # first run of the loop
384
+ if len(responses["items"]) == 0: # first run of the loop
383
385
  responses.update(partial)
384
386
  else:
385
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",
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: QTube
3
- Version: 2.5.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|
@@ -167,18 +175,24 @@ For more versatile uses, you can also use command line arguments with the [qtube
167
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.
168
176
 
169
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
+
170
179
  ### Requirements
180
+
171
181
  See the [requirements](requirements.txt) file or the [TOML](pyproject.toml) file.
172
182
 
173
183
  ## Examples
184
+
174
185
  This section presents examples of user parameters json files for concrete use-cases.
186
+
175
187
  * <a href="#example-1---every-videos-from-subscribed-channels">Every videos from subscribed channels</a>
176
188
  * <a href="#example-2---higher-quality-videos">Higher quality videos</a>
177
- * <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>
178
190
 
179
191
  ### Example 1 - Every videos from subscribed channels
192
+
180
193
  The following *user_params.json* file would add every new videos from channels you are subcribed to.
181
- ```
194
+
195
+ ```python
182
196
  {
183
197
  "required_in_channel_name": null,
184
198
  "banned_in_channel_name": null,
@@ -221,9 +235,12 @@ The following *user_params.json* file would add every new videos from channels y
221
235
  "verbosity": ["credentials","videos"]
222
236
  }
223
237
  ```
238
+
224
239
  ### Example 2 - Higher quality videos
240
+
225
241
  The following *user_params.json* file would only add videos with good quality.
226
- ```
242
+
243
+ ```python
227
244
  {
228
245
  "required_in_channel_name": null,
229
246
  "banned_in_channel_name": null,
@@ -266,9 +283,12 @@ The following *user_params.json* file would only add videos with good quality.
266
283
  "verbosity": ["credentials","videos"]
267
284
  }
268
285
  ```
286
+
269
287
  ### Example 3 - Specific video series from a creator
288
+
270
289
  The following *user_params.json* file would only add the *$1 vs.* MrBeast videos.
271
- ```
290
+
291
+ ```python
272
292
  {
273
293
  "required_in_channel_name": ["MrBeast"],
274
294
  "banned_in_channel_name": null,
@@ -313,13 +333,17 @@ The following *user_params.json* file would only add the *$1 vs.* MrBeast videos
313
333
  ```
314
334
 
315
335
  ## FAQ
336
+
316
337
  There are none yet. But don't hesitate to ask by sending me an [email](mailto:killian.lebreton35@gmail.com).
317
338
 
318
339
  ## Contact
340
+
319
341
  You can reach me by [email](mailto:killian.lebreton35@gmail.com). Please put *QTube* in the subject line.
320
342
 
321
343
  ## Acknowledgments
344
+
322
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.
323
346
 
324
347
  ## License
348
+
325
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,140 +0,0 @@
1
- import datetime as dt
2
-
3
-
4
- def get_recent_videos(youtube, playlist_ID: str, vid_nb: int) -> dict:
5
- """Retrieves the last videos added in a YT playlist.
6
-
7
- Args:
8
- youtube (Resource): YT API resource.
9
- playlist_ID (str): ID of the playlist.
10
- vid_nb (int): Number of videos to retrieve.
11
-
12
- Returns:
13
- recent_vids (dict): Dictionary containing the ID (keys) and upload date (values) of the last videos added in the playlist.
14
- """
15
- response = (
16
- youtube.playlistItems()
17
- .list(part="contentDetails", playlistId=playlist_ID, maxResults=vid_nb)
18
- .execute(num_retries=5)
19
- )
20
-
21
- recent_vids = {
22
- item["contentDetails"]["videoId"]: {
23
- "upload datetime": dt.datetime.fromisoformat(
24
- item["contentDetails"]["videoPublishedAt"]
25
- )
26
- }
27
- for item in response.get("items", [])
28
- }
29
-
30
- return recent_vids
31
-
32
-
33
- def get_playlist_content(youtube, playlist_ID: str) -> list[str]:
34
- """Retrieves the IDs of videos saved in a YT playlist.
35
-
36
- Args:
37
- youtube (Resource): YT API resource.
38
- playlist_ID (str): ID of the playlist.
39
-
40
- Returns:
41
- videos_IDs (list[str]): List containing the IDs of the videos saved in the playlist.
42
- """
43
- next_page_token = None
44
- videos_IDs = []
45
- while True:
46
- response = (
47
- youtube.playlistItems()
48
- .list(
49
- part="contentDetails",
50
- playlistId=playlist_ID,
51
- maxResults=50,
52
- pageToken=next_page_token,
53
- )
54
- .execute(num_retries=5)
55
- )
56
-
57
- temp_videos_IDs = [
58
- item["contentDetails"]["videoId"] for item in response.get("items")
59
- ]
60
- videos_IDs.extend(temp_videos_IDs)
61
-
62
- next_page_token = response.get("nextPageToken")
63
-
64
- if not next_page_token:
65
- break
66
-
67
- return videos_IDs
68
-
69
-
70
- def get_playlists_titles(youtube=None, playlist_IDs: list[str] = None) -> list[str]:
71
- """Retrieves the titles of a list of YT playlists.
72
-
73
- Args:
74
- youtube (Resource): YT API resource.
75
- playlists_IDs (list[str]): List of playlist IDs.
76
-
77
- Returns:
78
- titles (list[str]): List of YT playlist titles.
79
- """
80
- playlist_IDs_str = ",".join(playlist_IDs)
81
- response = (
82
- youtube.playlists()
83
- .list(part="snippet", id=playlist_IDs_str)
84
- .execute(num_retries=5)
85
- )
86
-
87
- titles = [playlist["snippet"]["title"] for playlist in response["items"]]
88
-
89
- return titles
90
-
91
-
92
- def get_playlists_video_counts(
93
- youtube=None, playlist_IDs: list[str] = None
94
- ) -> list[int]:
95
- """Retrieves the number of videos of a list of YT playlists.
96
-
97
- Args:
98
- youtube (Resource): YT API resource.
99
- playlists_IDs (list[str]): List of playlist IDs.
100
-
101
- Returns:
102
- counts (list[int]): List of YT playlist video counts.
103
- """
104
- playlist_IDs_str = ",".join(playlist_IDs)
105
- response = (
106
- youtube.playlists()
107
- .list(part="contentDetails", id=playlist_IDs_str)
108
- .execute(num_retries=5)
109
- )
110
-
111
- counts = [playlist["contentDetails"]["itemCount"] for playlist in response["items"]]
112
-
113
- return counts
114
-
115
-
116
- def add_to_playlist(youtube, playlist_ID: str, video_ID: str) -> None:
117
- """Adds a YT video to the YT playlist.
118
-
119
- Args:
120
- youtube (Resource): YT API resource.
121
- playlist_ID (str): Playlist ID.
122
- video_ID (str): Video ID.
123
-
124
- Returns:
125
- None
126
- """
127
- response = (
128
- youtube.playlistItems()
129
- .insert(
130
- part="snippet",
131
- body={
132
- "snippet": {
133
- "playlistId": playlist_ID,
134
- "resourceId": {"kind": "youtube#video", "videoId": video_ID},
135
- }
136
- },
137
- )
138
- .execute(num_retries=5)
139
- )
140
- 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=x-kzqfB54hzFTT7oolaTTn3Y-Jy9cM7W7L-fIlrj7Ls,35183
4
- QTube/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- QTube/utils/checks.py,sha256=w6_Mwhh1lpxYEQ6cusKtiOTtvrhDHFjNU5Yx43V3Ee0,13310
6
- QTube/utils/helpers.py,sha256=fl1_fePwy7ot-KRFB9Ieq7fMFtD8Q4TriK8cW17qPy0,9187
7
- QTube/utils/parsing.py,sha256=H1uVtevbDF-6JWy42--pDvkBBHzAvxBgujIErQk54AI,11057
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=tjfY-ohrQvk1BznbsdwQUcnX0Wq7rlBK43C-ZWOE4iY,3953
11
- QTube/utils/youtube/videos.py,sha256=YWhpLQVADr95lp1XMgnkg5z05YOxtBgpEauUz95zMeQ,20607
12
- QTube-2.5.0.dist-info/LICENSE.txt,sha256=cIZNbD-BZYZPzWYHhtE-iUCasUxQIwWzALL9nZh32pQ,1098
13
- QTube-2.5.0.dist-info/METADATA,sha256=5onw-FEmuyBiM62nsxA_iTUJCXSP8vc2j2ncI2Tjvkc,18001
14
- QTube-2.5.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
15
- QTube-2.5.0.dist-info/entry_points.txt,sha256=Q3SLDyRahuzrNnHC3UOkMH5gM_Um3eCLiLG4vSTPjqE,51
16
- QTube-2.5.0.dist-info/top_level.txt,sha256=z6oXrT8BiTTZuygjsbfe-NE4rmkHlDK8euAL9m6BT4A,6
17
- QTube-2.5.0.dist-info/RECORD,,