podflow 2025.1.26__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.
- Podflow/__init__.py +137 -0
- Podflow/basic/__init__.py +2 -0
- Podflow/basic/file_save.py +21 -0
- Podflow/basic/folder_build.py +16 -0
- Podflow/basic/get_duration.py +18 -0
- Podflow/basic/get_file_list.py +57 -0
- Podflow/basic/get_html_dict.py +30 -0
- Podflow/basic/http_client.py +75 -0
- Podflow/basic/list_merge_tidy.py +15 -0
- Podflow/basic/qr_code.py +55 -0
- Podflow/basic/split_dict.py +14 -0
- Podflow/basic/time_format.py +16 -0
- Podflow/basic/time_stamp.py +61 -0
- Podflow/basic/vary_replace.py +11 -0
- Podflow/basic/write_log.py +43 -0
- Podflow/bilibili/__init__.py +2 -0
- Podflow/bilibili/build.py +201 -0
- Podflow/bilibili/get.py +477 -0
- Podflow/bilibili/login.py +307 -0
- Podflow/config/__init__.py +2 -0
- Podflow/config/build_original.py +41 -0
- Podflow/config/channge_icon.py +163 -0
- Podflow/config/correct_channelid.py +230 -0
- Podflow/config/correct_config.py +103 -0
- Podflow/config/get_channelid.py +22 -0
- Podflow/config/get_channelid_id.py +21 -0
- Podflow/config/get_config.py +34 -0
- Podflow/download/__init__.py +2 -0
- Podflow/download/convert_bytes.py +18 -0
- Podflow/download/delete_part.py +20 -0
- Podflow/download/dl_aideo_video.py +307 -0
- Podflow/download/show_progress.py +46 -0
- Podflow/download/wait_animation.py +34 -0
- Podflow/download/youtube_and_bilibili_download.py +30 -0
- Podflow/ffmpeg_judge.py +45 -0
- Podflow/httpfs/__init__.py +2 -0
- Podflow/httpfs/app_bottle.py +212 -0
- Podflow/httpfs/port_judge.py +21 -0
- Podflow/main.py +248 -0
- Podflow/makeup/__init__.py +2 -0
- Podflow/makeup/del_makeup_yt_format_fail.py +19 -0
- Podflow/makeup/make_up_file.py +51 -0
- Podflow/makeup/make_up_file_format_mod.py +96 -0
- Podflow/makeup/make_up_file_mod.py +30 -0
- Podflow/message/__init__.py +2 -0
- Podflow/message/backup_zip_save.py +45 -0
- Podflow/message/create_main_rss.py +44 -0
- Podflow/message/display_qrcode_and_url.py +36 -0
- Podflow/message/fail_message_initialize.py +165 -0
- Podflow/message/format_time.py +27 -0
- Podflow/message/get_original_rss.py +65 -0
- Podflow/message/get_video_format.py +111 -0
- Podflow/message/get_video_format_multithread.py +42 -0
- Podflow/message/get_youtube_and_bilibili_video_format.py +87 -0
- Podflow/message/media_format.py +195 -0
- Podflow/message/original_rss_fail_print.py +15 -0
- Podflow/message/rss_create_hash.py +26 -0
- Podflow/message/title_correction.py +30 -0
- Podflow/message/update_information_display.py +72 -0
- Podflow/message/update_youtube_bilibili_rss.py +116 -0
- Podflow/message/want_retry.py +21 -0
- Podflow/message/xml_item.py +83 -0
- Podflow/message/xml_original_item.py +92 -0
- Podflow/message/xml_rss.py +46 -0
- Podflow/netscape/__init__.py +2 -0
- Podflow/netscape/bulid_netscape.py +44 -0
- Podflow/netscape/get_cookie_dict.py +21 -0
- Podflow/parse_arguments.py +80 -0
- Podflow/remove/__init__.py +2 -0
- Podflow/remove/remove_dir.py +33 -0
- Podflow/remove/remove_file.py +23 -0
- Podflow/youtube/__init__.py +2 -0
- Podflow/youtube/build.py +287 -0
- Podflow/youtube/get.py +376 -0
- Podflow/youtube/login.py +39 -0
- podflow-2025.1.26.dist-info/METADATA +214 -0
- podflow-2025.1.26.dist-info/RECORD +80 -0
- podflow-2025.1.26.dist-info/WHEEL +5 -0
- podflow-2025.1.26.dist-info/entry_points.txt +6 -0
- podflow-2025.1.26.dist-info/top_level.txt +1 -0
Podflow/youtube/get.py
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
# Podflow/youtube/get.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
import contextlib
|
5
|
+
import re
|
6
|
+
import os
|
7
|
+
import threading
|
8
|
+
from datetime import datetime
|
9
|
+
from Podflow import gVar
|
10
|
+
from Podflow.basic.http_client import http_client
|
11
|
+
from Podflow.basic.vary_replace import vary_replace
|
12
|
+
from Podflow.basic.get_html_dict import get_html_dict
|
13
|
+
from Podflow.basic.list_merge_tidy import list_merge_tidy
|
14
|
+
|
15
|
+
|
16
|
+
# 从YouTube播放列表获取更新模块
|
17
|
+
def get_youtube_html_playlists(
|
18
|
+
youtube_key, # YouTube 频道的唯一标识
|
19
|
+
youtube_value, # YouTube 账户或其他标识信息
|
20
|
+
guids=None, # 视频 ID 列表(用于比较已有视频)
|
21
|
+
direction_forward=True, # 控制获取方向,默认向前获取新的视频
|
22
|
+
update_size=20, # 更新数量限制,最多获取 20 个新视频
|
23
|
+
youtube_content_ytid_original=None, # 原始 YouTube 视频 ID 列表
|
24
|
+
):
|
25
|
+
idlist = [] # 存储新获取的 YouTube 视频 ID
|
26
|
+
item = {} # 存储视频信息(标题、描述、封面等)
|
27
|
+
threads = [] # 线程列表,用于并发获取视频详细信息
|
28
|
+
fail = [] # 存储获取失败的视频 ID
|
29
|
+
|
30
|
+
if guids is None:
|
31
|
+
guids = [""]
|
32
|
+
if youtube_content_ytid_original is None:
|
33
|
+
youtube_content_ytid_original = []
|
34
|
+
|
35
|
+
try:
|
36
|
+
videoid_start = guids[0] if direction_forward else guids[-1] # 获取起始视频 ID
|
37
|
+
except IndexError:
|
38
|
+
videoid_start = "" # 处理空列表情况,避免 IndexError
|
39
|
+
|
40
|
+
# 获取视频详细信息的内部函数
|
41
|
+
def get_video_item(videoid, youtube_value):
|
42
|
+
yt_Initial_Player_Response = get_html_dict(
|
43
|
+
f"https://www.youtube.com/watch?v={videoid}",
|
44
|
+
f"{youtube_value}|{videoid}",
|
45
|
+
"ytInitialPlayerResponse",
|
46
|
+
) # 解析 YouTube 页面,获取视频信息
|
47
|
+
if not yt_Initial_Player_Response:
|
48
|
+
return None # 若获取失败,则返回 None
|
49
|
+
|
50
|
+
try:
|
51
|
+
player_Microformat_Renderer = yt_Initial_Player_Response["microformat"][
|
52
|
+
"playerMicroformatRenderer"
|
53
|
+
]
|
54
|
+
except (KeyError, TypeError, IndexError, ValueError):
|
55
|
+
player_Microformat_Renderer = {} # 解析失败时,返回空字典
|
56
|
+
fail.append(videoid) # 记录失败的视频 ID
|
57
|
+
|
58
|
+
if player_Microformat_Renderer:
|
59
|
+
try:
|
60
|
+
item[videoid]["description"] = player_Microformat_Renderer[
|
61
|
+
"description"
|
62
|
+
]["simpleText"]
|
63
|
+
except (KeyError, TypeError, IndexError, ValueError):
|
64
|
+
item[videoid]["description"] = "" # 若没有描述,则置为空
|
65
|
+
item[videoid]["pubDate"] = player_Microformat_Renderer[
|
66
|
+
"publishDate"
|
67
|
+
] # 获取发布时间
|
68
|
+
item[videoid]["image"] = player_Microformat_Renderer["thumbnail"][
|
69
|
+
"thumbnails"
|
70
|
+
][0]["url"] # 获取封面图
|
71
|
+
with contextlib.suppress(KeyError, TypeError, IndexError, ValueError):
|
72
|
+
fail.remove(videoid) # 若成功获取,则从失败列表中移除
|
73
|
+
else:
|
74
|
+
return None # 若无有效数据,返回 None
|
75
|
+
|
76
|
+
# 获取播放列表数据
|
77
|
+
yt_initial_data = get_html_dict(
|
78
|
+
f"https://www.youtube.com/watch?v={videoid_start}&list=UULF{youtube_key[-22:]}",
|
79
|
+
f"{youtube_value} HTML",
|
80
|
+
"ytInitialData",
|
81
|
+
) # 解析 YouTube 播放列表页面,获取数据
|
82
|
+
if not yt_initial_data:
|
83
|
+
return None # 若获取失败,则返回 None
|
84
|
+
|
85
|
+
try:
|
86
|
+
playlists = yt_initial_data["contents"]["twoColumnWatchNextResults"][
|
87
|
+
"playlist"
|
88
|
+
]["playlist"]["contents"]
|
89
|
+
main_title = yt_initial_data["contents"]["twoColumnWatchNextResults"][
|
90
|
+
"playlist"
|
91
|
+
]["playlist"]["ownerName"]["simpleText"]
|
92
|
+
except (KeyError, TypeError, IndexError, ValueError):
|
93
|
+
return None # 若解析失败,返回 None
|
94
|
+
|
95
|
+
# 若方向是向前获取(最新视频)或没有起始视频 ID
|
96
|
+
if direction_forward or not videoid_start:
|
97
|
+
for playlist in playlists:
|
98
|
+
videoid = playlist["playlistPanelVideoRenderer"]["videoId"] # 提取视频 ID
|
99
|
+
if (
|
100
|
+
playlist["playlistPanelVideoRenderer"]["navigationEndpoint"][
|
101
|
+
"watchEndpoint"
|
102
|
+
]["index"]
|
103
|
+
== update_size
|
104
|
+
):
|
105
|
+
break # 如果达到更新上限,则停止
|
106
|
+
if videoid not in guids: # 确保视频 ID 不是已存在的
|
107
|
+
title = playlist["playlistPanelVideoRenderer"]["title"][
|
108
|
+
"simpleText"
|
109
|
+
] # 获取视频标题
|
110
|
+
idlist.append(videoid) # 添加到 ID 列表
|
111
|
+
item[videoid] = {"title": title, "yt-dlp": True} # 记录视频信息
|
112
|
+
if videoid in youtube_content_ytid_original: # 若视频已在原始列表中
|
113
|
+
item[videoid]["yt-dlp"] = False # 标记为已存在
|
114
|
+
item_thread = threading.Thread(
|
115
|
+
target=get_video_item, args=(videoid, youtube_value)
|
116
|
+
) # 启动线程获取详细信息
|
117
|
+
item_thread.start()
|
118
|
+
threads.append(item_thread)
|
119
|
+
else: # 处理向后获取(获取较旧的视频)
|
120
|
+
reversed_playlists = []
|
121
|
+
for playlist in reversed(playlists):
|
122
|
+
videoid = playlist["playlistPanelVideoRenderer"]["videoId"]
|
123
|
+
if videoid not in guids:
|
124
|
+
reversed_playlists.append(playlist) # 收集未存在的旧视频
|
125
|
+
else:
|
126
|
+
break # 如果找到已存在的视频 ID,则停止
|
127
|
+
|
128
|
+
for playlist in reversed(reversed_playlists[-update_size:]):
|
129
|
+
videoid = playlist["playlistPanelVideoRenderer"]["videoId"]
|
130
|
+
title = playlist["playlistPanelVideoRenderer"]["title"]["simpleText"]
|
131
|
+
idlist.append(videoid)
|
132
|
+
item[videoid] = {"title": title, "yt-dlp": True}
|
133
|
+
if videoid in youtube_content_ytid_original:
|
134
|
+
item[videoid]["yt-dlp"] = False
|
135
|
+
item_thread = threading.Thread(
|
136
|
+
target=get_video_item, args=(videoid, youtube_value)
|
137
|
+
)
|
138
|
+
item_thread.start()
|
139
|
+
threads.append(item_thread)
|
140
|
+
|
141
|
+
for thread in threads:
|
142
|
+
thread.join() # 等待所有线程完成
|
143
|
+
|
144
|
+
# 处理获取失败的视频
|
145
|
+
for videoid in fail:
|
146
|
+
get_video_item(videoid, youtube_value) # 重新尝试获取失败的视频
|
147
|
+
|
148
|
+
if fail: # 如果仍然有失败的视频
|
149
|
+
if direction_forward or not videoid_start:
|
150
|
+
for videoid in fail:
|
151
|
+
print(
|
152
|
+
f"{datetime.now().strftime('%H:%M:%S')}|{youtube_value}|{videoid} HTML无法更新, 将不获取"
|
153
|
+
)
|
154
|
+
if videoid in idlist:
|
155
|
+
idlist.remove(videoid) # 安全地移除视频 ID,避免 `ValueError`
|
156
|
+
del item[videoid] # 删除对应的字典项
|
157
|
+
else:
|
158
|
+
print(
|
159
|
+
f"{datetime.now().strftime('%H:%M:%S')}|{youtube_value} HTML有失败只更新部分"
|
160
|
+
)
|
161
|
+
index = len(idlist)
|
162
|
+
for videoid in fail:
|
163
|
+
if videoid in idlist:
|
164
|
+
index = min(idlist.index(videoid), index) # 计算最早失败视频的索引
|
165
|
+
idlist_fail = idlist[index:] # 截取失败的视频 ID 列表
|
166
|
+
idlist = idlist[:index] # 只保留成功的视频 ID
|
167
|
+
for videoid in idlist_fail:
|
168
|
+
if videoid in idlist:
|
169
|
+
idlist.remove(videoid) # 安全删除失败视频 ID
|
170
|
+
|
171
|
+
return {"list": idlist, "item": item, "title": main_title} # 返回最终结果
|
172
|
+
|
173
|
+
|
174
|
+
def get_youtube_shorts_id(youtube_key, youtube_value):
|
175
|
+
videoIds = []
|
176
|
+
url = f"https://www.youtube.com/channel/{youtube_key}/shorts"
|
177
|
+
if data := get_html_dict(url, youtube_value, "ytInitialData"):
|
178
|
+
with contextlib.suppress(KeyError, TypeError, IndexError, ValueError):
|
179
|
+
items = data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][2][
|
180
|
+
"tabRenderer"
|
181
|
+
]["content"]["richGridRenderer"]["contents"]
|
182
|
+
for item in items:
|
183
|
+
videoId = item["richItemRenderer"]["content"]["shortsLockupViewModel"][
|
184
|
+
"onTap"
|
185
|
+
]["innertubeCommand"]["reelWatchEndpoint"]["videoId"]
|
186
|
+
videoIds.append(videoId)
|
187
|
+
return videoIds
|
188
|
+
|
189
|
+
|
190
|
+
# 更新Youtube频道xml模块
|
191
|
+
def youtube_rss_update(
|
192
|
+
youtube_key,
|
193
|
+
youtube_value,
|
194
|
+
pattern_youtube_varys,
|
195
|
+
pattern_youtube404,
|
196
|
+
pattern_youtube_error,
|
197
|
+
):
|
198
|
+
channelid_youtube = gVar.channelid_youtube
|
199
|
+
channelid_youtube_rss = gVar.channelid_youtube_rss
|
200
|
+
channelid_youtube_ids_update = gVar.channelid_youtube_ids_update
|
201
|
+
# 获取已下载媒体名称
|
202
|
+
youtube_media = (
|
203
|
+
("m4a", "mp4") # 根据 channelid_youtube 的媒体类型选择文件格式
|
204
|
+
if channelid_youtube[youtube_value]["media"] == "m4a"
|
205
|
+
else ("mp4",) # 如果不是 m4a,则只选择 mp4
|
206
|
+
)
|
207
|
+
try:
|
208
|
+
# 遍历指定目录下的所有文件,筛选出以 youtube_media 结尾的文件
|
209
|
+
youtube_content_ytid_original = [
|
210
|
+
os.path.splitext(file)[0] # 获取文件名(不包括扩展名)
|
211
|
+
for file in os.listdir(f"channel_audiovisual/{youtube_key}") # 指定的目录
|
212
|
+
if file.endswith(youtube_media) # 筛选文件
|
213
|
+
]
|
214
|
+
except Exception:
|
215
|
+
# 如果发生异常,设置为空列表
|
216
|
+
youtube_content_ytid_original = []
|
217
|
+
try:
|
218
|
+
# 获取原始XML中的内容
|
219
|
+
original_item = gVar.xmls_original[youtube_key]
|
220
|
+
guids = re.findall(r"(?<=<guid>).+(?=</guid>)", original_item) # 查找所有guid
|
221
|
+
except KeyError:
|
222
|
+
# 如果没有找到对应的key,则guids为空
|
223
|
+
guids = []
|
224
|
+
# 构建 URL
|
225
|
+
youtube_url = f"https://www.youtube.com/feeds/videos.xml?channel_id={youtube_key}"
|
226
|
+
youtube_response = http_client(youtube_url, youtube_value) # 请求YouTube数据
|
227
|
+
youtube_html_playlists = None
|
228
|
+
youtube_channel_response = None
|
229
|
+
if youtube_response is not None and re.search(
|
230
|
+
pattern_youtube404, youtube_response.text, re.DOTALL
|
231
|
+
):
|
232
|
+
youtube_url = f"https://www.youtube.com/channel/{youtube_key}"
|
233
|
+
youtube_channel_response = http_client(youtube_url, f"{youtube_value} HTML")
|
234
|
+
if youtube_channel_response is not None:
|
235
|
+
pattern_youtube_error_mark = False
|
236
|
+
for pattern_youtube_error_key in pattern_youtube_error:
|
237
|
+
if pattern_youtube_error_key in youtube_channel_response.text:
|
238
|
+
pattern_youtube_error_mark = True
|
239
|
+
youtube_response = youtube_channel_response
|
240
|
+
break
|
241
|
+
if not pattern_youtube_error_mark:
|
242
|
+
# 检查响应是否有效,最多重试3次
|
243
|
+
for _ in range(3):
|
244
|
+
if youtube_html_playlists := get_youtube_html_playlists(
|
245
|
+
youtube_key,
|
246
|
+
youtube_value,
|
247
|
+
[
|
248
|
+
elem
|
249
|
+
for elem in guids
|
250
|
+
if elem in youtube_content_ytid_original
|
251
|
+
], # 仅选择已下载的guids
|
252
|
+
True,
|
253
|
+
channelid_youtube[youtube_value]["update_size"],
|
254
|
+
youtube_content_ytid_original,
|
255
|
+
):
|
256
|
+
break
|
257
|
+
shorts_ytid = []
|
258
|
+
elif youtube_response is not None and channelid_youtube[youtube_value]["NoShorts"]:
|
259
|
+
shorts_ytid = get_youtube_shorts_id(youtube_key, youtube_value)
|
260
|
+
gVar.video_id_failed += shorts_ytid # 将Shorts视频添加到失败列表中
|
261
|
+
else:
|
262
|
+
shorts_ytid = []
|
263
|
+
# 读取原Youtube频道xml文件并判断是否要更新
|
264
|
+
try:
|
265
|
+
with open(
|
266
|
+
f"channel_id/{youtube_key}.txt",
|
267
|
+
"r",
|
268
|
+
encoding="utf-8", # 以utf-8编码打开文件
|
269
|
+
) as file:
|
270
|
+
youtube_content_original = file.read() # 读取文件内容
|
271
|
+
youtube_content_original_clean = vary_replace(
|
272
|
+
pattern_youtube_varys, youtube_content_original
|
273
|
+
) # 清洗内容
|
274
|
+
except FileNotFoundError: # 如果文件不存在
|
275
|
+
youtube_content_original = None
|
276
|
+
youtube_content_original_clean = None
|
277
|
+
if youtube_html_playlists is not None: # 如果有新播放列表
|
278
|
+
channelid_youtube_rss[youtube_key] = {
|
279
|
+
"content": youtube_html_playlists,
|
280
|
+
"type": "dict",
|
281
|
+
}
|
282
|
+
if youtube_html_playlists["item"]:
|
283
|
+
channelid_youtube_ids_update[youtube_key] = youtube_value # 更新标识
|
284
|
+
youtube_content_ytid = youtube_html_playlists["list"] # 获取视频ID列表
|
285
|
+
else:
|
286
|
+
if youtube_response is not None:
|
287
|
+
# 如果没有新的播放列表,但响应有效
|
288
|
+
channelid_youtube_rss[youtube_key] = {
|
289
|
+
"content": youtube_response,
|
290
|
+
"type": "html",
|
291
|
+
}
|
292
|
+
youtube_content = youtube_response.text # 获取响应内容
|
293
|
+
if not youtube_channel_response:
|
294
|
+
youtube_content_clean = vary_replace(
|
295
|
+
pattern_youtube_varys, youtube_content
|
296
|
+
) # 清洗内容
|
297
|
+
if (
|
298
|
+
youtube_content_clean != youtube_content_original_clean
|
299
|
+
and youtube_response
|
300
|
+
): # 判断是否要更新
|
301
|
+
channelid_youtube_ids_update[youtube_key] = (
|
302
|
+
youtube_value # 更新标识
|
303
|
+
)
|
304
|
+
else:
|
305
|
+
# 如果没有响应,使用原始内容
|
306
|
+
channelid_youtube_rss[youtube_key] = {
|
307
|
+
"content": youtube_content_original,
|
308
|
+
"type": "text",
|
309
|
+
}
|
310
|
+
youtube_content = youtube_content_original
|
311
|
+
try:
|
312
|
+
# 从内容中提取视频ID
|
313
|
+
youtube_content_ytid = re.findall(
|
314
|
+
r"(?<=<id>yt:video:).{11}(?=</id>)", youtube_content
|
315
|
+
)
|
316
|
+
except TypeError:
|
317
|
+
youtube_content_ytid = [] # 处理类型错误
|
318
|
+
youtube_content_ytid = youtube_content_ytid[
|
319
|
+
: channelid_youtube[youtube_value][
|
320
|
+
"update_size"
|
321
|
+
] # 限制视频ID数量
|
322
|
+
]
|
323
|
+
youtube_content_new = list_merge_tidy(youtube_content_ytid, guids) # 合并并去重
|
324
|
+
if youtube_content_ytid := [
|
325
|
+
exclude
|
326
|
+
for exclude in youtube_content_ytid
|
327
|
+
if exclude not in youtube_content_ytid_original
|
328
|
+
and exclude not in shorts_ytid # 仅选择新视频ID(并且不是Shorts)
|
329
|
+
]:
|
330
|
+
channelid_youtube_ids_update[youtube_key] = youtube_value # 更新标识
|
331
|
+
gVar.youtube_content_ytid_update[youtube_key] = (
|
332
|
+
youtube_content_ytid # 保存更新的视频ID
|
333
|
+
)
|
334
|
+
# 向后更新
|
335
|
+
if channelid_youtube[youtube_value]["BackwardUpdate"] and guids:
|
336
|
+
# 计算向后更新的数量
|
337
|
+
backward_update_size = channelid_youtube[youtube_value]["last_size"] - len(
|
338
|
+
youtube_content_new
|
339
|
+
)
|
340
|
+
if backward_update_size > 0:
|
341
|
+
for _ in range(3):
|
342
|
+
# 获取历史播放列表
|
343
|
+
if youtube_html_backward_playlists := get_youtube_html_playlists(
|
344
|
+
youtube_key,
|
345
|
+
youtube_value,
|
346
|
+
guids,
|
347
|
+
False,
|
348
|
+
min(
|
349
|
+
backward_update_size,
|
350
|
+
channelid_youtube[youtube_value]["BackwardUpdate_size"],
|
351
|
+
),
|
352
|
+
youtube_content_ytid_original,
|
353
|
+
):
|
354
|
+
break
|
355
|
+
if youtube_html_backward_playlists:
|
356
|
+
backward_list = youtube_html_backward_playlists[
|
357
|
+
"list"
|
358
|
+
] # 获取向后更新的列表
|
359
|
+
for guid in backward_list.copy():
|
360
|
+
if guid in youtube_content_new:
|
361
|
+
backward_list.remove(guid) # 从列表中移除已更新的GUID
|
362
|
+
if youtube_html_backward_playlists and backward_list:
|
363
|
+
channelid_youtube_ids_update[youtube_key] = youtube_value # 更新标识
|
364
|
+
channelid_youtube_rss[youtube_key].update(
|
365
|
+
{"backward": youtube_html_backward_playlists}
|
366
|
+
) # 添加向后更新内容
|
367
|
+
youtube_content_ytid_backward = []
|
368
|
+
youtube_content_ytid_backward.extend(
|
369
|
+
guid
|
370
|
+
for guid in backward_list
|
371
|
+
if guid not in youtube_content_ytid_original
|
372
|
+
)
|
373
|
+
if youtube_content_ytid_backward:
|
374
|
+
gVar.youtube_content_ytid_backward_update[youtube_key] = (
|
375
|
+
youtube_content_ytid_backward # 保存向后更新的ID
|
376
|
+
)
|
Podflow/youtube/login.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Podflow/youtube/login.py
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
from datetime import datetime
|
5
|
+
from Podflow.basic.http_client import http_client
|
6
|
+
from Podflow.basic.write_log import write_log
|
7
|
+
from Podflow.netscape.get_cookie_dict import get_cookie_dict
|
8
|
+
|
9
|
+
|
10
|
+
def get_youtube_cookie_fail(arg0):
|
11
|
+
print(f"{datetime.now().strftime('%H:%M:%S')}{arg0}")
|
12
|
+
write_log("YouTube \033[31m获取cookie失败\033[0m")
|
13
|
+
return None
|
14
|
+
|
15
|
+
|
16
|
+
# 获取YouTube cookie模块
|
17
|
+
def get_youtube_cookie(channelid_youtube_ids):
|
18
|
+
if not channelid_youtube_ids:
|
19
|
+
return
|
20
|
+
youtube_cookie = get_cookie_dict("channel_data/yt_dlp_youtube.txt")
|
21
|
+
if youtube_cookie is None:
|
22
|
+
write_log("YouTube \033[31m获取cookie失败\033[0m")
|
23
|
+
return None
|
24
|
+
if response := http_client(
|
25
|
+
"https://www.youtube.com", "YouTube主页", 10, 4, True, youtube_cookie
|
26
|
+
):
|
27
|
+
html_content = response.text
|
28
|
+
if '"LOGGED_IN":true' in html_content:
|
29
|
+
print(
|
30
|
+
f"{datetime.now().strftime('%H:%M:%S')}|YouTube \033[32m获取cookie成功\033[0m"
|
31
|
+
)
|
32
|
+
return youtube_cookie
|
33
|
+
elif '"LOGGED_IN":false' in html_content:
|
34
|
+
return get_youtube_cookie_fail("|登陆YouTube失败")
|
35
|
+
else:
|
36
|
+
return get_youtube_cookie_fail("|登陆YouTube无法判断")
|
37
|
+
else:
|
38
|
+
write_log("YouTube \033[31m获取cookie失败\033[0m")
|
39
|
+
return None
|
@@ -0,0 +1,214 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: podflow
|
3
|
+
Version: 2025.1.26
|
4
|
+
Summary: A podcast server that includes YouTube and BiliBili
|
5
|
+
Home-page: https://github.com/gruel-zxz/podflow
|
6
|
+
Author: gruel_zxz
|
7
|
+
Author-email: zhuxizhouzxz@gmail.com
|
8
|
+
License: UNKNOWN
|
9
|
+
Platform: UNKNOWN
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Requires-Python: >=3.8
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
Requires-Dist: astral>=3.2
|
16
|
+
Requires-Dist: bottle>=0.13.2
|
17
|
+
Requires-Dist: qrcode>=8.0
|
18
|
+
Requires-Dist: yt-dlp>=2025.1.15
|
19
|
+
Requires-Dist: chardet>=5.2.0
|
20
|
+
Requires-Dist: cherrypy>=18.10.0
|
21
|
+
Requires-Dist: requests>=2.32.3
|
22
|
+
Requires-Dist: pycryptodome>=3.21.0
|
23
|
+
Requires-Dist: ffmpeg-python>=0.2.0
|
24
|
+
Requires-Dist: BeautifulSoup4>=4.12.3
|
25
|
+
|
26
|
+
# Podflow
|
27
|
+
|
28
|
+
<img src='https://raw.githubusercontent.com/gruel-zxz/podflow/main/Podflow.png' alt='logo' style='width: 150px; height: 150px;'/>
|
29
|
+
|
30
|
+
建立Podcast服务器,用于下载YouTube和哔哩哔哩的音视频并导入到Podcast中。
|
31
|
+
|
32
|
+
需要新建[配置文件](#主配置), 或通过首次运行后自动生成
|
33
|
+
|
34
|
+
YouTube的cookies需要使用chrome插件导出Netscape格式并保存到channel_data文件夹中
|
35
|
+
|
36
|
+
PS:可能存在大量未知bug,改进中并尝试加入抖音……
|
37
|
+
|
38
|
+
### 安装方法
|
39
|
+
|
40
|
+
需要`Python 3.8`以上环境,并安装ffmpeg (安装方法: <https://ffmpeg.org/>)。
|
41
|
+
|
42
|
+
Podflow可以使用以下命令进行安装
|
43
|
+
|
44
|
+
```
|
45
|
+
pip install Podflow
|
46
|
+
```
|
47
|
+
|
48
|
+
在iOS上可以使用[Shortcuts](<https://apps.apple.com/us/app/shortcuts/id915249334/>)运行Podflow, 需要用到[a-shell](<https://apps.apple.com/us/app/a-shell/id1473805438/>)
|
49
|
+
和[捷径脚本](<https://www.icloud.com/shortcuts/54213ea7e46b4b21b7a0bce02f9c64a1/>)
|
50
|
+
|
51
|
+
### 命令行参数说明
|
52
|
+
|
53
|
+
| 参数 | 选项 | 类型 | 默认值 | 描述 |
|
54
|
+
|----------------|-------------------|-----------------|-----------------|--------------------------------------------------|
|
55
|
+
| `-n` | `--times` | `int` | +∞ | 次数 |
|
56
|
+
| `-d` | `--delay` | `int` | 1500 | 延迟(单位: 秒) |
|
57
|
+
| `-c` | `--config` | `string` | "config.json" | 配置文件的路径 |
|
58
|
+
| `--shortcuts` | | `string` | [] | 仅适用于捷径APP |
|
59
|
+
| `--httpfs` | | `boolean` | 无 | 仅启用服务器功能, 不更新频道 |
|
60
|
+
|
61
|
+
### 使用示例
|
62
|
+
|
63
|
+
你可以使用以下命令行格式来运行程序:
|
64
|
+
|
65
|
+
```
|
66
|
+
Podflow -n 24 -d 3600
|
67
|
+
```
|
68
|
+
|
69
|
+
---
|
70
|
+
|
71
|
+
### 主配置
|
72
|
+
|
73
|
+
| 参数 | 类型 | 默认值 | 描述 |
|
74
|
+
| --------------------- | ----------- | ------------------------------------- | -------------------------------------------------------------------------------------------- |
|
75
|
+
| `channelid_youtube` | `dict` | `{...}` | 可以按[YouTube频道配置](#YouTube频道配置)编写, 也可以按`{"youtube": "UCBR8-60-B28hp2BmDPdntcQ"}`编写, 其他参数会按默认值自动补全 |
|
76
|
+
| `channelid_bilibili` | `dict` | `{...}` | 可以按[哔哩哔哩频道配置](#哔哩哔哩频道配置)编写, 也可以按`{"哔哩哔哩弹幕网": "8047632"}`编写, 其他参数会按默认值自动补全 |
|
77
|
+
| `preparation_per_count` | `int` | `100` | 获取媒体信息每组数量 |
|
78
|
+
| `completion_count` | `int` | `100` | 媒体缺失时最大补全数量 |
|
79
|
+
| `retry_count` | `int` | `5` | 媒体下载重试次数 |
|
80
|
+
| `url` | `string` | `"http://127.0.0.1"` | HTTP共享地址 |
|
81
|
+
| `port` | `int` | `8000` | HTTP共享端口 |
|
82
|
+
| `port_in_url` | `boolean` | `true` | HTTP共享地址是否包含端口 |
|
83
|
+
| `httpfs` | `boolean` | `false` | HTTP共享日志 |
|
84
|
+
| `title` | `string` | `"Podflow"` | 博客的名称 |
|
85
|
+
| `filename` | `string` | `"Podflow"` | 主XML的文件名称 |
|
86
|
+
| `link` | `string` | `"https://github.com/gruel-zxz/podflow"` | 博客主页 |
|
87
|
+
| `description` | `string` | `"在iOS平台上借助workflow和a-shell搭建专属的播客服务器。"` | 博客信息 |
|
88
|
+
| `icon` | `string` | `"https://raw.githubusercontent.com/gruel-zxz/podflow/main/Podflow.png"` | 博客图标 |
|
89
|
+
| `category` | `string` | `"TV & Film"` | 博客类型 |
|
90
|
+
| `token` | `string` | `""` | token认证, 如为null或""将不启用token |
|
91
|
+
| `delete_incompletement` | `boolean` | `false` | 是否删除下载中断媒体(下载前处理流程) |
|
92
|
+
| `remove_media` | `boolean` | `true` | 是否删除无用的媒体文件 |
|
93
|
+
|
94
|
+
---
|
95
|
+
|
96
|
+
### YouTube频道配置
|
97
|
+
|
98
|
+
| 参数 | 类型 | 默认值 | 描述 |
|
99
|
+
| --------------------- | ----------- | ------------------------------------- | -------------------------------------------------------------- |
|
100
|
+
| `id` | `string` | `"UCBR8-60-B28hp2BmDPdntcQ"` | 频道ID |
|
101
|
+
| `title` | `string` | `"YouTube"` | 频道名称 |
|
102
|
+
| `update_size` | `int` | `15` | 每次获取频道媒体数量 |
|
103
|
+
| `quality` | `string` | `"480"` | 媒体分辨率(仅在media为视频时有效) |
|
104
|
+
| `last_size` | `int` | `50` | 媒体保留数量 |
|
105
|
+
| `media` | `string` | `"m4a"` | 下载媒体类型 |
|
106
|
+
| `DisplayRSSaddress` | `boolean` | `false` | 是否在Print中显示子博客地址 |
|
107
|
+
| `InmainRSS` | `boolean` | `true` | 是否在主博客中 |
|
108
|
+
| `QRcode` | `boolean` | `false` | 是否显示子博客地址二维码(仅在DisplayRSSaddress为True时有效) |
|
109
|
+
| `BackwardUpdate` | `boolean` | `false` | 是否向后更新 |
|
110
|
+
| `BackwardUpdate_size` | `int` | `3` | 向后更新数量(仅在BackwardUpdate为True时有效) |
|
111
|
+
| `want_retry_count` | `int` | `8` | 媒体获取失败后多少次后重试 |
|
112
|
+
| `NoShorts` | `boolean` | `false` | 是否不下载Shorts媒体 |
|
113
|
+
| `title_change` | `list` | `[]` | 标题文本修改规则 |
|
114
|
+
|
115
|
+
---
|
116
|
+
|
117
|
+
### 哔哩哔哩频道配置
|
118
|
+
|
119
|
+
| 参数 | 类型 | 默认值 | 描述 |
|
120
|
+
| --------------------- | ----------- | ------------------------------------- | -------------------------------------------------------------- |
|
121
|
+
| `id` | `string` | `"8047632"` | 频道ID |
|
122
|
+
| `title` | `string` | `"哔哩哔哩弹幕网"` | 频道名称 |
|
123
|
+
| `update_size` | `int` | `25` | 每次获取频道媒体数量 |
|
124
|
+
| `quality` | `string` | `"480"` | 媒体分辨率(仅在media为视频时有效) |
|
125
|
+
| `last_size` | `int` | `100` | 媒体保留数量 |
|
126
|
+
| `media` | `string` | `"m4a"` | 下载媒体类型 |
|
127
|
+
| `DisplayRSSaddress` | `boolean` | `false` | 是否在Print中显示子博客地址 |
|
128
|
+
| `InmainRSS` | `boolean` | `true` | 是否在主博客中 |
|
129
|
+
| `QRcode` | `boolean` | `false` | 是否显示子博客地址二维码(仅在DisplayRSSaddress为True时有效) |
|
130
|
+
| `BackwardUpdate` | `boolean` | `false` | 是否向后更新 |
|
131
|
+
| `BackwardUpdate_size` | `int` | `3` | 向后更新数量(仅在BackwardUpdate为True时有效) |
|
132
|
+
| `want_retry_count` | `int` | `8` | 媒体获取失败后多少次后重试 |
|
133
|
+
| `AllPartGet` | `boolean` | `false` | 是否提前获取分P或互动视频 |
|
134
|
+
| `title_change` | `dict` | `{}` | 标题文本修改规则 |
|
135
|
+
|
136
|
+
---
|
137
|
+
|
138
|
+
### 配置文件参考
|
139
|
+
|
140
|
+
```json
|
141
|
+
{
|
142
|
+
"preparation_per_count": 100,
|
143
|
+
"completion_count": 100,
|
144
|
+
"retry_count": 5,
|
145
|
+
"url": "http://127.0.0.1",
|
146
|
+
"port": 8000,
|
147
|
+
"port_in_url": true,
|
148
|
+
"httpfs": false,
|
149
|
+
"title": "Podflow",
|
150
|
+
"filename": "Podflow",
|
151
|
+
"link": "https://github.com/gruel-zxz/podflow",
|
152
|
+
"description": "在iOS平台上借助workflow和a-shell搭建专属的播客服务器。",
|
153
|
+
"icon": "https://raw.githubusercontent.com/gruel-zxz/podflow/main/Podflow.png",
|
154
|
+
"category": "TV & Film",
|
155
|
+
"token": "",
|
156
|
+
"delete_incompletement": false,
|
157
|
+
"remove_media": true,
|
158
|
+
"channelid_youtube": {
|
159
|
+
"youtube": {
|
160
|
+
"update_size": 15,
|
161
|
+
"id": "UCBR8-60-B28hp2BmDPdntcQ",
|
162
|
+
"title": "YouTube",
|
163
|
+
"quality": "480",
|
164
|
+
"last_size": 50,
|
165
|
+
"media": "m4a",
|
166
|
+
"DisplayRSSaddress": false,
|
167
|
+
"InmainRSS": true,
|
168
|
+
"QRcode": false,
|
169
|
+
"BackwardUpdate": false,
|
170
|
+
"BackwardUpdate_size": 3,
|
171
|
+
"want_retry_count": 8,
|
172
|
+
"title_change": [
|
173
|
+
{
|
174
|
+
"mode": "add-left",
|
175
|
+
"match": "",
|
176
|
+
"url": "https://www.youtube.com/playlist?list=...",
|
177
|
+
"text": ""
|
178
|
+
},
|
179
|
+
{
|
180
|
+
"mode": "add-right",
|
181
|
+
"match": "",
|
182
|
+
"url": "",
|
183
|
+
"text": ""
|
184
|
+
}
|
185
|
+
],
|
186
|
+
"NoShorts": false
|
187
|
+
}
|
188
|
+
},
|
189
|
+
"channelid_bilibili": {
|
190
|
+
"哔哩哔哩弹幕网": {
|
191
|
+
"update_size": 25,
|
192
|
+
"id": "8047632",
|
193
|
+
"title": "哔哩哔哩弹幕网",
|
194
|
+
"quality": "480",
|
195
|
+
"last_size": 100,
|
196
|
+
"media": "m4a",
|
197
|
+
"DisplayRSSaddress": false,
|
198
|
+
"InmainRSS": true,
|
199
|
+
"QRcode": false,
|
200
|
+
"BackwardUpdate": false,
|
201
|
+
"BackwardUpdate_size": 3,
|
202
|
+
"want_retry_count": 8,
|
203
|
+
"title_change": {
|
204
|
+
"mode": "replace",
|
205
|
+
"match": "",
|
206
|
+
"text": ""
|
207
|
+
},
|
208
|
+
"AllPartGet": false
|
209
|
+
}
|
210
|
+
}
|
211
|
+
}
|
212
|
+
```
|
213
|
+
|
214
|
+
|