StreamingCommunity 2.5.2__py3-none-any.whl → 2.5.5__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.

Potentially problematic release.


This version of StreamingCommunity might be problematic. Click here for more details.

Files changed (268) hide show
  1. StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py +143 -143
  2. StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +136 -136
  3. StreamingCommunity/Api/Player/ddl.py +89 -89
  4. StreamingCommunity/Api/Player/maxstream.py +151 -151
  5. StreamingCommunity/Api/Player/supervideo.py +193 -193
  6. StreamingCommunity/Api/Player/vixcloud.py +272 -272
  7. StreamingCommunity/Api/Site/1337xx/__init__.py +51 -50
  8. StreamingCommunity/Api/Site/1337xx/costant.py +14 -14
  9. StreamingCommunity/Api/Site/1337xx/site.py +87 -89
  10. StreamingCommunity/Api/Site/1337xx/title.py +63 -64
  11. StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py +74 -50
  12. StreamingCommunity/Api/Site/altadefinizionegratis/costant.py +21 -19
  13. StreamingCommunity/Api/Site/altadefinizionegratis/film.py +81 -72
  14. StreamingCommunity/Api/Site/altadefinizionegratis/site.py +116 -94
  15. StreamingCommunity/Api/Site/animeunity/__init__.py +75 -50
  16. StreamingCommunity/Api/Site/animeunity/costant.py +21 -19
  17. StreamingCommunity/Api/Site/animeunity/film_serie.py +171 -134
  18. StreamingCommunity/Api/Site/animeunity/site.py +191 -174
  19. StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +97 -97
  20. StreamingCommunity/Api/Site/cb01new/__init__.py +51 -51
  21. StreamingCommunity/Api/Site/cb01new/costant.py +19 -19
  22. StreamingCommunity/Api/Site/cb01new/film.py +61 -71
  23. StreamingCommunity/Api/Site/cb01new/site.py +82 -82
  24. StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +55 -55
  25. StreamingCommunity/Api/Site/ddlstreamitaly/costant.py +20 -20
  26. StreamingCommunity/Api/Site/ddlstreamitaly/series.py +149 -145
  27. StreamingCommunity/Api/Site/ddlstreamitaly/site.py +98 -98
  28. StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +84 -84
  29. StreamingCommunity/Api/Site/guardaserie/__init__.py +50 -50
  30. StreamingCommunity/Api/Site/guardaserie/costant.py +19 -19
  31. StreamingCommunity/Api/Site/guardaserie/series.py +199 -198
  32. StreamingCommunity/Api/Site/guardaserie/site.py +89 -89
  33. StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +110 -110
  34. StreamingCommunity/Api/Site/ilcorsaronero/__init__.py +51 -51
  35. StreamingCommunity/Api/Site/ilcorsaronero/costant.py +18 -18
  36. StreamingCommunity/Api/Site/ilcorsaronero/site.py +71 -71
  37. StreamingCommunity/Api/Site/ilcorsaronero/title.py +44 -44
  38. StreamingCommunity/Api/Site/ilcorsaronero/util/ilCorsarScraper.py +149 -149
  39. StreamingCommunity/Api/Site/mostraguarda/__init__.py +48 -48
  40. StreamingCommunity/Api/Site/mostraguarda/costant.py +18 -18
  41. StreamingCommunity/Api/Site/mostraguarda/film.py +90 -101
  42. StreamingCommunity/Api/Site/streamingcommunity/__init__.py +79 -55
  43. StreamingCommunity/Api/Site/streamingcommunity/costant.py +21 -19
  44. StreamingCommunity/Api/Site/streamingcommunity/film.py +86 -75
  45. StreamingCommunity/Api/Site/streamingcommunity/series.py +259 -207
  46. StreamingCommunity/Api/Site/streamingcommunity/site.py +156 -142
  47. StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +124 -124
  48. StreamingCommunity/Api/Template/Class/SearchType.py +101 -101
  49. StreamingCommunity/Api/Template/Util/__init__.py +4 -4
  50. StreamingCommunity/Api/Template/Util/get_domain.py +201 -201
  51. StreamingCommunity/Api/Template/Util/manage_ep.py +178 -178
  52. StreamingCommunity/Api/Template/Util/recall_search.py +37 -37
  53. StreamingCommunity/Api/Template/__init__.py +2 -2
  54. StreamingCommunity/Api/Template/site.py +87 -87
  55. StreamingCommunity/Lib/Downloader/HLS/downloader.py +529 -1008
  56. StreamingCommunity/Lib/Downloader/HLS/proxyes.py +110 -110
  57. StreamingCommunity/Lib/Downloader/HLS/segments.py +446 -573
  58. StreamingCommunity/Lib/Downloader/MP4/downloader.py +181 -155
  59. StreamingCommunity/Lib/Downloader/TOR/downloader.py +297 -295
  60. StreamingCommunity/Lib/Downloader/__init__.py +4 -4
  61. StreamingCommunity/Lib/FFmpeg/__init__.py +4 -4
  62. StreamingCommunity/Lib/FFmpeg/capture.py +170 -170
  63. StreamingCommunity/Lib/FFmpeg/command.py +264 -296
  64. StreamingCommunity/Lib/FFmpeg/util.py +248 -248
  65. StreamingCommunity/Lib/M3U8/__init__.py +5 -5
  66. StreamingCommunity/Lib/M3U8/decryptor.py +164 -164
  67. StreamingCommunity/Lib/M3U8/estimator.py +146 -228
  68. StreamingCommunity/Lib/M3U8/parser.py +666 -666
  69. StreamingCommunity/Lib/M3U8/url_fixer.py +57 -57
  70. StreamingCommunity/Lib/TMBD/__init__.py +1 -1
  71. StreamingCommunity/Lib/TMBD/obj_tmbd.py +39 -39
  72. StreamingCommunity/Lib/TMBD/tmdb.py +345 -345
  73. StreamingCommunity/TelegramHelp/__init__.py +0 -0
  74. StreamingCommunity/TelegramHelp/request_manager.py +82 -0
  75. StreamingCommunity/TelegramHelp/session.py +56 -0
  76. StreamingCommunity/TelegramHelp/telegram_bot.py +561 -0
  77. StreamingCommunity/Upload/update.py +75 -67
  78. StreamingCommunity/Upload/version.py +5 -5
  79. StreamingCommunity/Util/_jsonConfig.py +227 -228
  80. StreamingCommunity/Util/call_stack.py +42 -42
  81. StreamingCommunity/Util/color.py +20 -20
  82. StreamingCommunity/Util/console.py +12 -12
  83. StreamingCommunity/Util/ffmpeg_installer.py +342 -370
  84. StreamingCommunity/Util/headers.py +159 -159
  85. StreamingCommunity/Util/logger.py +61 -61
  86. StreamingCommunity/Util/message.py +36 -64
  87. StreamingCommunity/Util/os.py +500 -507
  88. StreamingCommunity/Util/table.py +271 -228
  89. StreamingCommunity/run.py +352 -245
  90. {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/LICENSE +674 -674
  91. {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/METADATA +601 -543
  92. StreamingCommunity-2.5.5.dist-info/RECORD +96 -0
  93. {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/entry_points.txt +0 -1
  94. StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/js_parser.cpython-313.pyc +0 -0
  95. StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/js_parser.cpython-39.pyc +0 -0
  96. StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/util.cpython-313.pyc +0 -0
  97. StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/util.cpython-39.pyc +0 -0
  98. StreamingCommunity/Api/Player/__pycache__/ddl.cpython-313.pyc +0 -0
  99. StreamingCommunity/Api/Player/__pycache__/ddl.cpython-39.pyc +0 -0
  100. StreamingCommunity/Api/Player/__pycache__/maxstream.cpython-313.pyc +0 -0
  101. StreamingCommunity/Api/Player/__pycache__/maxstream.cpython-39.pyc +0 -0
  102. StreamingCommunity/Api/Player/__pycache__/supervideo.cpython-313.pyc +0 -0
  103. StreamingCommunity/Api/Player/__pycache__/supervideo.cpython-39.pyc +0 -0
  104. StreamingCommunity/Api/Player/__pycache__/vixcloud.cpython-313.pyc +0 -0
  105. StreamingCommunity/Api/Player/__pycache__/vixcloud.cpython-39.pyc +0 -0
  106. StreamingCommunity/Api/Site/1337xx/__pycache__/__init__.cpython-313.pyc +0 -0
  107. StreamingCommunity/Api/Site/1337xx/__pycache__/__init__.cpython-39.pyc +0 -0
  108. StreamingCommunity/Api/Site/1337xx/__pycache__/costant.cpython-313.pyc +0 -0
  109. StreamingCommunity/Api/Site/1337xx/__pycache__/costant.cpython-39.pyc +0 -0
  110. StreamingCommunity/Api/Site/1337xx/__pycache__/site.cpython-313.pyc +0 -0
  111. StreamingCommunity/Api/Site/1337xx/__pycache__/site.cpython-39.pyc +0 -0
  112. StreamingCommunity/Api/Site/1337xx/__pycache__/title.cpython-313.pyc +0 -0
  113. StreamingCommunity/Api/Site/1337xx/__pycache__/title.cpython-39.pyc +0 -0
  114. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/__init__.cpython-313.pyc +0 -0
  115. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/__init__.cpython-39.pyc +0 -0
  116. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/costant.cpython-313.pyc +0 -0
  117. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/costant.cpython-39.pyc +0 -0
  118. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/film.cpython-313.pyc +0 -0
  119. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/film.cpython-39.pyc +0 -0
  120. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/site.cpython-313.pyc +0 -0
  121. StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/site.cpython-39.pyc +0 -0
  122. StreamingCommunity/Api/Site/animeunity/__pycache__/__init__.cpython-313.pyc +0 -0
  123. StreamingCommunity/Api/Site/animeunity/__pycache__/__init__.cpython-39.pyc +0 -0
  124. StreamingCommunity/Api/Site/animeunity/__pycache__/costant.cpython-313.pyc +0 -0
  125. StreamingCommunity/Api/Site/animeunity/__pycache__/costant.cpython-39.pyc +0 -0
  126. StreamingCommunity/Api/Site/animeunity/__pycache__/film_serie.cpython-313.pyc +0 -0
  127. StreamingCommunity/Api/Site/animeunity/__pycache__/film_serie.cpython-39.pyc +0 -0
  128. StreamingCommunity/Api/Site/animeunity/__pycache__/site.cpython-313.pyc +0 -0
  129. StreamingCommunity/Api/Site/animeunity/__pycache__/site.cpython-39.pyc +0 -0
  130. StreamingCommunity/Api/Site/animeunity/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
  131. StreamingCommunity/Api/Site/animeunity/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
  132. StreamingCommunity/Api/Site/cb01new/__pycache__/__init__.cpython-313.pyc +0 -0
  133. StreamingCommunity/Api/Site/cb01new/__pycache__/__init__.cpython-39.pyc +0 -0
  134. StreamingCommunity/Api/Site/cb01new/__pycache__/costant.cpython-313.pyc +0 -0
  135. StreamingCommunity/Api/Site/cb01new/__pycache__/costant.cpython-39.pyc +0 -0
  136. StreamingCommunity/Api/Site/cb01new/__pycache__/film.cpython-313.pyc +0 -0
  137. StreamingCommunity/Api/Site/cb01new/__pycache__/film.cpython-39.pyc +0 -0
  138. StreamingCommunity/Api/Site/cb01new/__pycache__/site.cpython-313.pyc +0 -0
  139. StreamingCommunity/Api/Site/cb01new/__pycache__/site.cpython-39.pyc +0 -0
  140. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/__init__.cpython-313.pyc +0 -0
  141. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/__init__.cpython-39.pyc +0 -0
  142. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/costant.cpython-313.pyc +0 -0
  143. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/costant.cpython-39.pyc +0 -0
  144. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/series.cpython-313.pyc +0 -0
  145. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/series.cpython-39.pyc +0 -0
  146. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/site.cpython-313.pyc +0 -0
  147. StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/site.cpython-39.pyc +0 -0
  148. StreamingCommunity/Api/Site/ddlstreamitaly/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
  149. StreamingCommunity/Api/Site/ddlstreamitaly/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
  150. StreamingCommunity/Api/Site/guardaserie/__pycache__/__init__.cpython-313.pyc +0 -0
  151. StreamingCommunity/Api/Site/guardaserie/__pycache__/__init__.cpython-39.pyc +0 -0
  152. StreamingCommunity/Api/Site/guardaserie/__pycache__/costant.cpython-313.pyc +0 -0
  153. StreamingCommunity/Api/Site/guardaserie/__pycache__/costant.cpython-39.pyc +0 -0
  154. StreamingCommunity/Api/Site/guardaserie/__pycache__/series.cpython-313.pyc +0 -0
  155. StreamingCommunity/Api/Site/guardaserie/__pycache__/series.cpython-39.pyc +0 -0
  156. StreamingCommunity/Api/Site/guardaserie/__pycache__/site.cpython-313.pyc +0 -0
  157. StreamingCommunity/Api/Site/guardaserie/__pycache__/site.cpython-39.pyc +0 -0
  158. StreamingCommunity/Api/Site/guardaserie/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
  159. StreamingCommunity/Api/Site/guardaserie/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
  160. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/__init__.cpython-313.pyc +0 -0
  161. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/__init__.cpython-39.pyc +0 -0
  162. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/costant.cpython-313.pyc +0 -0
  163. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/costant.cpython-39.pyc +0 -0
  164. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/site.cpython-313.pyc +0 -0
  165. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/site.cpython-39.pyc +0 -0
  166. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/title.cpython-313.pyc +0 -0
  167. StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/title.cpython-39.pyc +0 -0
  168. StreamingCommunity/Api/Site/ilcorsaronero/util/__pycache__/ilCorsarScraper.cpython-313.pyc +0 -0
  169. StreamingCommunity/Api/Site/ilcorsaronero/util/__pycache__/ilCorsarScraper.cpython-39.pyc +0 -0
  170. StreamingCommunity/Api/Site/mostraguarda/__pycache__/__init__.cpython-313.pyc +0 -0
  171. StreamingCommunity/Api/Site/mostraguarda/__pycache__/__init__.cpython-39.pyc +0 -0
  172. StreamingCommunity/Api/Site/mostraguarda/__pycache__/costant.cpython-313.pyc +0 -0
  173. StreamingCommunity/Api/Site/mostraguarda/__pycache__/costant.cpython-39.pyc +0 -0
  174. StreamingCommunity/Api/Site/mostraguarda/__pycache__/film.cpython-313.pyc +0 -0
  175. StreamingCommunity/Api/Site/mostraguarda/__pycache__/film.cpython-39.pyc +0 -0
  176. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/__init__.cpython-313.pyc +0 -0
  177. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/__init__.cpython-39.pyc +0 -0
  178. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/costant.cpython-313.pyc +0 -0
  179. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/costant.cpython-39.pyc +0 -0
  180. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/film.cpython-313.pyc +0 -0
  181. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/film.cpython-39.pyc +0 -0
  182. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/series.cpython-313.pyc +0 -0
  183. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/series.cpython-39.pyc +0 -0
  184. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/site.cpython-313.pyc +0 -0
  185. StreamingCommunity/Api/Site/streamingcommunity/__pycache__/site.cpython-39.pyc +0 -0
  186. StreamingCommunity/Api/Site/streamingcommunity/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
  187. StreamingCommunity/Api/Site/streamingcommunity/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
  188. StreamingCommunity/Api/Template/Class/__pycache__/SearchType.cpython-313.pyc +0 -0
  189. StreamingCommunity/Api/Template/Class/__pycache__/SearchType.cpython-39.pyc +0 -0
  190. StreamingCommunity/Api/Template/Util/__pycache__/__init__.cpython-313.pyc +0 -0
  191. StreamingCommunity/Api/Template/Util/__pycache__/__init__.cpython-39.pyc +0 -0
  192. StreamingCommunity/Api/Template/Util/__pycache__/get_domain.cpython-313.pyc +0 -0
  193. StreamingCommunity/Api/Template/Util/__pycache__/get_domain.cpython-39.pyc +0 -0
  194. StreamingCommunity/Api/Template/Util/__pycache__/manage_ep.cpython-313.pyc +0 -0
  195. StreamingCommunity/Api/Template/Util/__pycache__/manage_ep.cpython-39.pyc +0 -0
  196. StreamingCommunity/Api/Template/Util/__pycache__/recall_search.cpython-313.pyc +0 -0
  197. StreamingCommunity/Api/Template/Util/__pycache__/recall_search.cpython-39.pyc +0 -0
  198. StreamingCommunity/Api/Template/__pycache__/__init__.cpython-313.pyc +0 -0
  199. StreamingCommunity/Api/Template/__pycache__/__init__.cpython-39.pyc +0 -0
  200. StreamingCommunity/Api/Template/__pycache__/site.cpython-313.pyc +0 -0
  201. StreamingCommunity/Api/Template/__pycache__/site.cpython-39.pyc +0 -0
  202. StreamingCommunity/Lib/Downloader/HLS/__pycache__/downloader.cpython-313.pyc +0 -0
  203. StreamingCommunity/Lib/Downloader/HLS/__pycache__/downloader.cpython-39.pyc +0 -0
  204. StreamingCommunity/Lib/Downloader/HLS/__pycache__/proxyes.cpython-313.pyc +0 -0
  205. StreamingCommunity/Lib/Downloader/HLS/__pycache__/proxyes.cpython-39.pyc +0 -0
  206. StreamingCommunity/Lib/Downloader/HLS/__pycache__/segments.cpython-313.pyc +0 -0
  207. StreamingCommunity/Lib/Downloader/HLS/__pycache__/segments.cpython-39.pyc +0 -0
  208. StreamingCommunity/Lib/Downloader/MP4/__pycache__/downloader.cpython-313.pyc +0 -0
  209. StreamingCommunity/Lib/Downloader/MP4/__pycache__/downloader.cpython-39.pyc +0 -0
  210. StreamingCommunity/Lib/Downloader/TOR/__pycache__/downloader.cpython-313.pyc +0 -0
  211. StreamingCommunity/Lib/Downloader/TOR/__pycache__/downloader.cpython-39.pyc +0 -0
  212. StreamingCommunity/Lib/Downloader/__pycache__/__init__.cpython-313.pyc +0 -0
  213. StreamingCommunity/Lib/Downloader/__pycache__/__init__.cpython-39.pyc +0 -0
  214. StreamingCommunity/Lib/FFmpeg/__pycache__/__init__.cpython-313.pyc +0 -0
  215. StreamingCommunity/Lib/FFmpeg/__pycache__/__init__.cpython-39.pyc +0 -0
  216. StreamingCommunity/Lib/FFmpeg/__pycache__/capture.cpython-313.pyc +0 -0
  217. StreamingCommunity/Lib/FFmpeg/__pycache__/capture.cpython-39.pyc +0 -0
  218. StreamingCommunity/Lib/FFmpeg/__pycache__/command.cpython-313.pyc +0 -0
  219. StreamingCommunity/Lib/FFmpeg/__pycache__/command.cpython-39.pyc +0 -0
  220. StreamingCommunity/Lib/FFmpeg/__pycache__/util.cpython-313.pyc +0 -0
  221. StreamingCommunity/Lib/FFmpeg/__pycache__/util.cpython-39.pyc +0 -0
  222. StreamingCommunity/Lib/M3U8/__pycache__/__init__.cpython-313.pyc +0 -0
  223. StreamingCommunity/Lib/M3U8/__pycache__/__init__.cpython-39.pyc +0 -0
  224. StreamingCommunity/Lib/M3U8/__pycache__/decryptor.cpython-313.pyc +0 -0
  225. StreamingCommunity/Lib/M3U8/__pycache__/decryptor.cpython-39.pyc +0 -0
  226. StreamingCommunity/Lib/M3U8/__pycache__/estimator.cpython-313.pyc +0 -0
  227. StreamingCommunity/Lib/M3U8/__pycache__/estimator.cpython-39.pyc +0 -0
  228. StreamingCommunity/Lib/M3U8/__pycache__/parser.cpython-313.pyc +0 -0
  229. StreamingCommunity/Lib/M3U8/__pycache__/parser.cpython-39.pyc +0 -0
  230. StreamingCommunity/Lib/M3U8/__pycache__/url_fixer.cpython-313.pyc +0 -0
  231. StreamingCommunity/Lib/M3U8/__pycache__/url_fixer.cpython-39.pyc +0 -0
  232. StreamingCommunity/Lib/TMBD/__pycache__/__init__.cpython-313.pyc +0 -0
  233. StreamingCommunity/Lib/TMBD/__pycache__/__init__.cpython-39.pyc +0 -0
  234. StreamingCommunity/Lib/TMBD/__pycache__/obj_tmbd.cpython-313.pyc +0 -0
  235. StreamingCommunity/Lib/TMBD/__pycache__/obj_tmbd.cpython-39.pyc +0 -0
  236. StreamingCommunity/Lib/TMBD/__pycache__/tmdb.cpython-313.pyc +0 -0
  237. StreamingCommunity/Lib/TMBD/__pycache__/tmdb.cpython-39.pyc +0 -0
  238. StreamingCommunity/Upload/__pycache__/update.cpython-313.pyc +0 -0
  239. StreamingCommunity/Upload/__pycache__/update.cpython-39.pyc +0 -0
  240. StreamingCommunity/Upload/__pycache__/version.cpython-313.pyc +0 -0
  241. StreamingCommunity/Upload/__pycache__/version.cpython-39.pyc +0 -0
  242. StreamingCommunity/Util/__pycache__/_jsonConfig.cpython-313.pyc +0 -0
  243. StreamingCommunity/Util/__pycache__/_jsonConfig.cpython-39.pyc +0 -0
  244. StreamingCommunity/Util/__pycache__/call_stack.cpython-313.pyc +0 -0
  245. StreamingCommunity/Util/__pycache__/call_stack.cpython-39.pyc +0 -0
  246. StreamingCommunity/Util/__pycache__/color.cpython-313.pyc +0 -0
  247. StreamingCommunity/Util/__pycache__/color.cpython-39.pyc +0 -0
  248. StreamingCommunity/Util/__pycache__/console.cpython-313.pyc +0 -0
  249. StreamingCommunity/Util/__pycache__/console.cpython-39.pyc +0 -0
  250. StreamingCommunity/Util/__pycache__/ffmpeg_installer.cpython-313.pyc +0 -0
  251. StreamingCommunity/Util/__pycache__/ffmpeg_installer.cpython-39.pyc +0 -0
  252. StreamingCommunity/Util/__pycache__/headers.cpython-313.pyc +0 -0
  253. StreamingCommunity/Util/__pycache__/headers.cpython-39.pyc +0 -0
  254. StreamingCommunity/Util/__pycache__/logger.cpython-313.pyc +0 -0
  255. StreamingCommunity/Util/__pycache__/logger.cpython-39.pyc +0 -0
  256. StreamingCommunity/Util/__pycache__/message.cpython-313.pyc +0 -0
  257. StreamingCommunity/Util/__pycache__/message.cpython-39.pyc +0 -0
  258. StreamingCommunity/Util/__pycache__/os.cpython-313.pyc +0 -0
  259. StreamingCommunity/Util/__pycache__/os.cpython-39.pyc +0 -0
  260. StreamingCommunity/Util/__pycache__/table.cpython-313.pyc +0 -0
  261. StreamingCommunity/Util/__pycache__/table.cpython-39.pyc +0 -0
  262. StreamingCommunity/__pycache__/__init__.cpython-313.pyc +0 -0
  263. StreamingCommunity/__pycache__/__init__.cpython-39.pyc +0 -0
  264. StreamingCommunity/__pycache__/run.cpython-313.pyc +0 -0
  265. StreamingCommunity/__pycache__/run.cpython-39.pyc +0 -0
  266. StreamingCommunity-2.5.2.dist-info/RECORD +0 -264
  267. {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/WHEEL +0 -0
  268. {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/top_level.txt +0 -0
@@ -1,1008 +1,529 @@
1
- # 17.10.24
2
-
3
- import os
4
- import sys
5
- import time
6
- import logging
7
-
8
-
9
- # External libraries
10
- import httpx
11
-
12
-
13
- # Internal utilities
14
- from StreamingCommunity.Util._jsonConfig import config_manager
15
- from StreamingCommunity.Util.headers import get_headers
16
- from StreamingCommunity.Util.console import console, Panel
17
- from StreamingCommunity.Util.color import Colors
18
- from StreamingCommunity.Util.os import (
19
- compute_sha1_hash,
20
- os_manager,
21
- internet_manager
22
- )
23
-
24
- # Logic class
25
- from ...FFmpeg import (
26
- print_duration_table,
27
- join_video,
28
- join_audios,
29
- join_subtitle
30
- )
31
- from ...M3U8 import (
32
- M3U8_Parser,
33
- M3U8_Codec,
34
- M3U8_UrlFix
35
- )
36
- from .segments import M3U8_Segments
37
-
38
-
39
- # Config
40
- DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
41
- DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
42
- DOWNLOAD_VIDEO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_video')
43
- DOWNLOAD_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_audio')
44
- MERGE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_audio')
45
- DOWNLOAD_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_sub')
46
- MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
47
- REMOVE_SEGMENTS_FOLDER = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
48
- FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolution')
49
- GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
50
- RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
51
-
52
-
53
- # Variable
54
- max_timeout = config_manager.get_int("REQUESTS", "timeout")
55
- m3u8_url_fixer = M3U8_UrlFix()
56
- list_MissingTs = []
57
-
58
-
59
-
60
- class PathManager:
61
- def __init__(self, output_filename):
62
- """
63
- Initializes the PathManager with the output filename.
64
-
65
- Args:
66
- output_filename (str): The name of the output file (should end with .mp4).
67
- """
68
- self.output_filename = output_filename
69
-
70
- # Create the base path by removing the '.mp4' extension from the output filename
71
- self.base_path = str(output_filename).replace(".mp4", "")
72
- logging.info(f"class 'PathManager'; set base path: {self.base_path}")
73
-
74
- # Define the path for a temporary directory where segments will be stored
75
- self.base_temp = os.path.join(self.base_path, "tmp")
76
- self.video_segments_path = os.path.join(self.base_temp, "video")
77
- self.audio_segments_path = os.path.join(self.base_temp, "audio")
78
- self.subtitle_segments_path = os.path.join(self.base_temp, "subtitle")
79
-
80
- def create_directories(self):
81
- """
82
- Creates the necessary directories for storing video, audio, and subtitle segments.
83
- """
84
-
85
- os.makedirs(self.base_temp, exist_ok=True)
86
- os.makedirs(self.video_segments_path, exist_ok=True)
87
- os.makedirs(self.audio_segments_path, exist_ok=True)
88
- os.makedirs(self.subtitle_segments_path, exist_ok=True)
89
-
90
-
91
- class HttpClient:
92
- def __init__(self, headers: str = None):
93
- """
94
- Initializes the HttpClient with specified headers.
95
- """
96
- self.headers = headers
97
-
98
- def get(self, url: str):
99
- """
100
- Sends a GET request to the specified URL and returns the response as text.
101
-
102
- Returns:
103
- str: The response body as text if the request is successful, None otherwise.
104
- """
105
- logging.info(f"class 'HttpClient'; make request: {url}")
106
- try:
107
- response = httpx.get(
108
- url=url,
109
- headers=self.headers,
110
- timeout=max_timeout,
111
- follow_redirects=True
112
- )
113
-
114
- response.raise_for_status()
115
- return response.text
116
-
117
- except Exception as e:
118
- console.print(f"Request to {url} failed with error: {e}")
119
- return 404
120
-
121
- def get_content(self, url):
122
- """
123
- Sends a GET request to the specified URL and returns the raw response content.
124
-
125
- Returns:
126
- bytes: The response content as bytes if the request is successful, None otherwise.
127
- """
128
- logging.info(f"class 'HttpClient'; make request: {url}")
129
- try:
130
- response = httpx.get(
131
- url=url,
132
- headers=self.headers,
133
- timeout=max_timeout
134
- )
135
-
136
- response.raise_for_status()
137
- return response.content # Return the raw response content
138
-
139
- except Exception as e:
140
- logging.error(f"Request to {url} failed: {response.status_code} when get content.")
141
- return None
142
-
143
-
144
- class ContentExtractor:
145
- def __init__(self):
146
- """
147
- This class is responsible for extracting audio, subtitle, and video information from an M3U8 playlist.
148
- """
149
- pass
150
-
151
- def start(self, obj_parse: M3U8_Parser):
152
- """
153
- Starts the extraction process by parsing the M3U8 playlist and collecting audio, subtitle, and video data.
154
-
155
- Args:
156
- obj_parse (str): The M3U8_Parser obj of the M3U8 playlist.
157
- """
158
-
159
- self.obj_parse = obj_parse
160
-
161
- # Collect audio, subtitle, and video information
162
- self._collect_audio()
163
- self._collect_subtitle()
164
- self._collect_video()
165
-
166
- def _collect_audio(self):
167
- """
168
- It checks for available audio languages and the specific audio tracks to download.
169
- """
170
- logging.info(f"class 'ContentExtractor'; call _collect_audio()")
171
-
172
- # Collect available audio tracks and their corresponding URIs and names
173
- self.list_available_audio = self.obj_parse._audio.get_all_uris_and_names()
174
-
175
- # Check if there are any audio tracks available; if not, disable download
176
- if self.list_available_audio is not None:
177
-
178
- # Extract available languages from the audio tracks
179
- available_languages = [obj_audio.get('language') for obj_audio in self.list_available_audio]
180
- set_language = DOWNLOAD_SPECIFIC_AUDIO
181
- downloadable_languages = list(set(available_languages) & set(set_language))
182
-
183
- # Only show if there is something available
184
- if len(available_languages) > 0:
185
- console.print(f"[cyan bold]Audio →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_languages)}[/purple] | "
186
- f"[red]Set:[/red] [purple]{', '.join(set_language)}[/purple] | "
187
- f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_languages)}[/purple]")
188
-
189
- else:
190
- console.log("[red]Can't find a list of audios")
191
-
192
- def _collect_subtitle(self):
193
- """
194
- It checks for available subtitle languages and the specific subtitles to download.
195
- """
196
- logging.info(f"class 'ContentExtractor'; call _collect_subtitle()")
197
-
198
- # Collect available subtitles and their corresponding URIs and names
199
- self.list_available_subtitles = self.obj_parse._subtitle.get_all_uris_and_names()
200
-
201
- # Check if there are any subtitles available; if not, disable download
202
- if self.list_available_subtitles is not None:
203
-
204
- # Extract available languages from the subtitles
205
- available_languages = [obj_subtitle.get('language') for obj_subtitle in self.list_available_subtitles]
206
- set_language = DOWNLOAD_SPECIFIC_SUBTITLE
207
- downloadable_languages = list(set(available_languages) & set(set_language))
208
-
209
- # Only show if there is something available
210
- if len(available_languages) > 0:
211
- console.print(f"[cyan bold]Subtitle →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_languages)}[/purple] | "
212
- f"[red]Set:[/red] [purple]{', '.join(set_language)}[/purple] | "
213
- f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_languages)}[/purple]")
214
-
215
- else:
216
- console.log("[red]Can't find a list of subtitles")
217
-
218
- def _collect_video(self):
219
- """
220
- It identifies the best video quality and displays relevant information to the user.
221
- """
222
- logging.info(f"class 'ContentExtractor'; call _collect_video()")
223
- set_resolution = "Best"
224
-
225
- # Collect custom quality video if a specific resolution is set
226
- if FILTER_CUSTOM_REOLUTION != -1:
227
- self.m3u8_index, video_res = self.obj_parse._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
228
- set_resolution = f"{FILTER_CUSTOM_REOLUTION}p"
229
-
230
- else:
231
-
232
- # Otherwise, get the best available video quality
233
- self.m3u8_index, video_res = self.obj_parse._video.get_best_uri()
234
-
235
- self.codec: M3U8_Codec = self.obj_parse.codec
236
-
237
- # List all available resolutions
238
- tuple_available_resolution = self.obj_parse._video.get_list_resolution()
239
- list_available_resolution = [str(resolution[0]) + "x" + str(resolution[1]) for resolution in tuple_available_resolution]
240
- logging.info(f"M3U8 index selected: {self.m3u8_index}, with resolution: {video_res}")
241
-
242
- # Create a formatted table to display video info
243
- console.print(f"[cyan bold]Video →[/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
244
- f"[red]Set:[/red] [purple]{set_resolution}[/purple] | "
245
- f"[yellow]Downloadable:[/yellow] [purple]{video_res[0]}x{video_res[1]}[/purple]")
246
-
247
- if self.codec is not None:
248
-
249
- # Generate the string for available codec information
250
- available_codec_info = (
251
- f"[green]v[/green]: [yellow]{self.codec.video_codec_name}[/yellow] "
252
- f"([green]b[/green]: [yellow]{self.codec.video_bitrate // 1000}k[/yellow]), "
253
- f"[green]a[/green]: [yellow]{self.codec.audio_codec_name}[/yellow] "
254
- f"([green]b[/green]: [yellow]{self.codec.audio_bitrate // 1000}k[/yellow])"
255
- )
256
-
257
- # Determine what to display for "Set"
258
- # If the codec usage is enabled in the configuration, use the detailed codec info
259
- # Otherwise, display "copy"
260
- if config_manager.get_bool("M3U8_CONVERSION", "use_codec"):
261
- set_codec_info = available_codec_info
262
- else:
263
- set_codec_info = "[purple]copy[/purple]"
264
-
265
- # Print the formatted result with "Available" and "Set" information
266
- console.print(
267
- f"[bold cyan]Codec →[/bold cyan] [green]Available:[/green] {available_codec_info} | "
268
- f"[red]Set:[/red] {set_codec_info}"
269
- )
270
-
271
-
272
- # Fix the URL if it does not include the full protocol
273
- if "http" not in self.m3u8_index:
274
-
275
- # Generate the full URL
276
- self.m3u8_index = m3u8_url_fixer.generate_full_url(self.m3u8_index)
277
- logging.info(f"Generated index URL: {self.m3u8_index}")
278
-
279
- # Check if a valid HTTPS URL is obtained
280
- if self.m3u8_index is not None and "https" in self.m3u8_index:
281
- #console.print(f"[cyan]Found m3u8 index [white]=> [red]{self.m3u8_index}")
282
- print()
283
-
284
- else:
285
- logging.error("[download_m3u8] Can't find a valid m3u8 index")
286
- raise ValueError("Invalid m3u8 index URL")
287
-
288
- print("")
289
-
290
-
291
- class DownloadTracker:
292
- def __init__(self, path_manager: PathManager):
293
- """
294
- Initializes the DownloadTracker with paths for audio, subtitle, and video segments.
295
-
296
- Args:
297
- path_manager (PathManager): An instance of the PathManager class to manage file paths.
298
- """
299
-
300
- # Initialize lists to track downloaded audio, subtitles, and video
301
- self.downloaded_audio = []
302
- self.downloaded_subtitle = []
303
- self.downloaded_video = []
304
-
305
- self.video_segment_path = path_manager.video_segments_path
306
- self.audio_segments_path = path_manager.audio_segments_path
307
- self.subtitle_segments_path = path_manager.subtitle_segments_path
308
-
309
- def add_video(self, available_video):
310
- """
311
- Adds a single video to the list of downloaded videos.
312
-
313
- Args:
314
- available_video (str): The URL of the video to be downloaded.
315
- """
316
- logging.info(f"class 'DownloadTracker'; call add_video() with parameter: {available_video}")
317
-
318
- self.downloaded_video.append({
319
- 'type': 'video',
320
- 'url': available_video,
321
- 'path': os.path.join(self.video_segment_path, "0.ts")
322
- })
323
-
324
- def add_audio(self, list_available_audio):
325
- """
326
- Adds available audio tracks to the list of downloaded audio.
327
-
328
- Args:
329
- list_available_audio (list): A list of available audio track objects.
330
- """
331
- logging.info(f"class 'DownloadTracker'; call add_audio() with parameter: {list_available_audio}")
332
-
333
- for obj_audio in list_available_audio:
334
-
335
- # Check if specific audio languages are set for download
336
- if len(DOWNLOAD_SPECIFIC_AUDIO) > 0:
337
-
338
- # Skip this audio track if its language is not in the specified list
339
- if obj_audio.get('language') not in DOWNLOAD_SPECIFIC_AUDIO:
340
- continue
341
-
342
- # Construct the full path for the audio segment directory
343
- full_path_audio = os.path.join(self.audio_segments_path, obj_audio.get('language'))
344
-
345
- # Append the audio information to the downloaded audio list
346
- self.downloaded_audio.append({
347
- 'type': 'audio',
348
- 'url': obj_audio.get('uri'),
349
- 'language': obj_audio.get('language'),
350
- 'path': os.path.join(full_path_audio, "0.ts")
351
- })
352
-
353
- def add_subtitle(self, list_available_subtitles):
354
- """
355
- Adds available subtitles to the list of downloaded subtitles.
356
-
357
- Args:
358
- list_available_subtitles (list): A list of available subtitle objects.
359
- """
360
- logging.info(f"class 'DownloadTracker'; call add_subtitle() with parameter: {list_available_subtitles}")
361
-
362
- for obj_subtitle in list_available_subtitles:
363
-
364
- # Check if specific subtitle languages are set for download
365
- if len(DOWNLOAD_SPECIFIC_SUBTITLE) > 0:
366
-
367
- # Skip this subtitle if its language is not in the specified list
368
- if obj_subtitle.get('language') not in DOWNLOAD_SPECIFIC_SUBTITLE:
369
- continue
370
-
371
- sub_language = obj_subtitle.get('language')
372
-
373
- # Construct the full path for the subtitle file
374
- sub_full_path = os.path.join(self.subtitle_segments_path, sub_language + ".vtt")
375
-
376
- self.downloaded_subtitle.append({
377
- 'type': 'sub',
378
- 'url': obj_subtitle.get('uri'),
379
- 'language': obj_subtitle.get('language'),
380
- 'path': sub_full_path
381
- })
382
-
383
-
384
- class ContentDownloader:
385
- def __init__(self):
386
- """
387
- Initializes the ContentDownloader class.
388
-
389
- Attributes:
390
- expected_real_time (float): Expected real-time duration of the video download.
391
- """
392
- self.expected_real_time = None
393
-
394
- def download_video(self, downloaded_video):
395
- """
396
- Downloads the video if it doesn't already exist.
397
-
398
- Args:
399
- downloaded_video (list): A list containing information about the video to download.
400
- """
401
- logging.info(f"class 'ContentDownloader'; call download_video() with parameter: {downloaded_video}")
402
-
403
- # Check if the video file already exists
404
- if not os.path.exists(downloaded_video[0].get('path')):
405
- folder_name = os.path.dirname(downloaded_video[0].get('path'))
406
-
407
- # Create an instance of M3U8_Segments to handle video segments download
408
- video_m3u8 = M3U8_Segments(downloaded_video[0].get('url'), folder_name)
409
-
410
- # Get information about the video segments (e.g., duration, ts files to download)
411
- video_m3u8.get_info()
412
-
413
- # Store the expected real-time duration of the video
414
- self.expected_real_time = video_m3u8.expected_real_time
415
-
416
- # Download the video streams and print status
417
- info_dw = video_m3u8.download_streams(f"{Colors.MAGENTA}video", "video")
418
- list_MissingTs.append(info_dw)
419
-
420
- # Print duration information of the downloaded video
421
- #print_duration_table(downloaded_video[0].get('path'))
422
-
423
- else:
424
- console.log("[cyan]Video [red]already exists.")
425
-
426
- def download_audio(self, downloaded_audio):
427
- """
428
- Downloads audio tracks if they don't already exist.
429
-
430
- Args:
431
- downloaded_audio (list): A list containing information about audio tracks to download.
432
- """
433
- logging.info(f"class 'ContentDownloader'; call download_audio() with parameter: {downloaded_audio}")
434
-
435
- for obj_audio in downloaded_audio:
436
- folder_name = os.path.dirname(obj_audio.get('path'))
437
-
438
- # Check if the audio file already exists
439
- if not os.path.exists(obj_audio.get('path')):
440
-
441
- # Create an instance of M3U8_Segments to handle audio segments download
442
- audio_m3u8 = M3U8_Segments(obj_audio.get('url'), folder_name)
443
-
444
- # Get information about the audio segments (e.g., duration, ts files to download)
445
- audio_m3u8.get_info()
446
-
447
- # Download the audio segments and print status
448
- info_dw = audio_m3u8.download_streams(f"{Colors.MAGENTA}audio {Colors.RED}{obj_audio.get('language')}", f"audio_{obj_audio.get('language')}")
449
- list_MissingTs.append(info_dw)
450
-
451
- # Print duration information of the downloaded audio
452
- #print_duration_table(obj_audio.get('path'))
453
-
454
- else:
455
- console.log(f"[cyan]Audio [white]([green]{obj_audio.get('language')}[white]) [red]already exists.")
456
-
457
- def download_subtitle(self, downloaded_subtitle):
458
- """
459
- Downloads subtitle files if they don't already exist.
460
-
461
- Args:
462
- downloaded_subtitle (list): A list containing information about subtitles to download.
463
- """
464
- logging.info(f"class 'ContentDownloader'; call download_subtitle() with parameter: {downloaded_subtitle}")
465
-
466
- for obj_subtitle in downloaded_subtitle:
467
- sub_language = obj_subtitle.get('language')
468
-
469
- # Check if the subtitle file already exists
470
- if os.path.exists(obj_subtitle.get("path")):
471
- console.log(f"[cyan]Subtitle [white]([green]{sub_language}[white]) [red]already exists.")
472
- continue # Skip to the next subtitle if it exists
473
-
474
- # Parse the M3U8 file to get the subtitle URI
475
- m3u8_sub_parser = M3U8_Parser()
476
- url = obj_subtitle.get('url')
477
- success = False
478
-
479
- for attempt in range(RETRY_LIMIT):
480
- try:
481
- response = httpx.get(url, headers={'user-agent': get_headers()}, timeout=20)
482
- response.raise_for_status()
483
- m3u8_sub_parser.parse_data(
484
- uri=obj_subtitle.get('uri'),
485
- raw_content=response.text
486
- )
487
- success = True
488
- break
489
-
490
- except httpx.RequestError as e:
491
- logging.warning(f"Attempt {attempt + 1} failed for URL {url}: {e}")
492
- time.sleep(2)
493
-
494
- if not success:
495
- console.log(f"[red]Failed to download subtitle data for: {sub_language}")
496
- continue
497
-
498
- # Print the status of the subtitle download
499
- console.print(f"[cyan] - Downloading subtitle: [red]{sub_language.lower()}")
500
-
501
- # Download the subtitle content with retry
502
- subtitle_content = None
503
- for attempt in range(RETRY_LIMIT):
504
- try:
505
- response = httpx.get(m3u8_sub_parser.subtitle[-1], headers={'user-agent': get_headers()}, timeout=20)
506
- response.raise_for_status()
507
- subtitle_content = response.content
508
- break
509
-
510
- except httpx.RequestError as e:
511
- logging.warning(f"Attempt {attempt + 1} failed for subtitle content URL: {e}")
512
- time.sleep(2)
513
-
514
- if subtitle_content is None:
515
- console.log(f"[red]Failed to download subtitle content for: {sub_language}")
516
- continue
517
-
518
- # Write the content to the specified file
519
- try:
520
- with open(obj_subtitle.get("path"), "wb") as f:
521
- f.write(subtitle_content)
522
- #console.log(f"[green]Subtitle downloaded successfully: {sub_language}")
523
-
524
- except Exception as e:
525
- logging.error(f"Failed to write subtitle file for {sub_language}: {e}")
526
- #console.log(f"[red]Error writing subtitle file: {sub_language}")
527
-
528
-
529
- class ContentJoiner:
530
- def __init__(self, path_manager):
531
- """
532
- Initializes the ContentJoiner class.
533
-
534
- Args:
535
- path_manager (PathManager): An instance of PathManager to manage output paths.
536
- """
537
- self.path_manager: PathManager = path_manager
538
-
539
- def setup(self, downloaded_video, downloaded_audio, downloaded_subtitle, codec = None):
540
- """
541
- Sets up the content joiner with downloaded media files.
542
-
543
- Args:
544
- downloaded_video (list): List of downloaded video information.
545
- downloaded_audio (list): List of downloaded audio information.
546
- downloaded_subtitle (list): List of downloaded subtitle information.
547
- """
548
- self.downloaded_video = downloaded_video
549
- self.downloaded_audio = downloaded_audio
550
- self.downloaded_subtitle = downloaded_subtitle
551
- self.codec = codec
552
-
553
- # Initialize flags to check if media is available
554
- self.converted_out_path = None
555
- self.there_is_video = len(downloaded_video) > 0
556
- self.there_is_audio = len(downloaded_audio) > 0
557
- self.there_is_subtitle = len(downloaded_subtitle) > 0
558
-
559
- # Start the joining process
560
- self.conversione()
561
-
562
- def conversione(self):
563
- """
564
- Handles the joining of video, audio, and subtitles based on availability.
565
- """
566
-
567
- # Join audio and video if audio is available
568
- if self.there_is_audio:
569
- if MERGE_AUDIO:
570
-
571
- # Join video with audio tracks
572
- self.converted_out_path = self._join_video_audio()
573
-
574
- else:
575
-
576
- # Process each available audio track
577
- for obj_audio in self.downloaded_audio:
578
- language = obj_audio.get('language')
579
- path = obj_audio.get('path')
580
-
581
- # Set the new path for regular audio
582
- new_path = self.path_manager.output_filename.replace(".mp4", f"_{language}.mp4")
583
-
584
- try:
585
-
586
- # Rename the audio file to the new path
587
- os.rename(path, new_path)
588
- logging.info(f"Audio moved to {new_path}")
589
-
590
- except Exception as e:
591
- logging.error(f"Failed to move audio {path} to {new_path}: {e}")
592
-
593
- # Convert video if available
594
- if self.there_is_video:
595
- self.converted_out_path = self._join_video()
596
-
597
- # If no audio but video is available, join video
598
- else:
599
- if self.there_is_video:
600
- self.converted_out_path = self._join_video()
601
-
602
- # Join subtitles if available
603
- if self.there_is_subtitle:
604
- if MERGE_SUBTITLE:
605
- if self.converted_out_path is not None:
606
- self.converted_out_path = self._join_video_subtitles(self.converted_out_path)
607
-
608
- else:
609
-
610
- # Process each available subtitle track
611
- for obj_sub in self.downloaded_subtitle:
612
- language = obj_sub.get('language')
613
- path = obj_sub.get('path')
614
- forced = 'forced' in language
615
-
616
- # Adjust the language name and set the new path based on forced status
617
- if forced:
618
- language = language.replace("forced-", "")
619
- new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.forced.vtt")
620
- else:
621
- new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.vtt")
622
-
623
- try:
624
- # Rename the subtitle file to the new path
625
- os.rename(path, new_path)
626
- logging.info(f"Subtitle moved to {new_path}")
627
-
628
- except Exception as e:
629
- logging.error(f"Failed to move subtitle {path} to {new_path}: {e}")
630
-
631
- def _join_video(self):
632
- """
633
- Joins video segments into a single video file.
634
-
635
- Returns:
636
- str: The path to the joined video file.
637
- """
638
- path_join_video = os.path.join(self.path_manager.base_path, "v_v.mp4")
639
- logging.info(f"JOIN video path: {path_join_video}")
640
-
641
- # Check if the joined video file already exists
642
- if not os.path.exists(path_join_video):
643
-
644
- # Join the video segments into a single video file
645
- join_video(
646
- video_path=self.downloaded_video[0].get('path'),
647
- out_path=path_join_video,
648
- codec=self.codec
649
- )
650
-
651
- else:
652
- console.log("[red]Output join video already exists.")
653
-
654
- return path_join_video
655
-
656
- def _join_video_audio(self):
657
- """
658
- Joins video segments with audio tracks into a single video with audio file.
659
-
660
- Returns:
661
- str: The path to the joined video with audio file.
662
- """
663
- path_join_video_audio = os.path.join(self.path_manager.base_path, "v_a.mp4")
664
- logging.info(f"JOIN audio path: {path_join_video_audio}")
665
-
666
- # Check if the joined video with audio file already exists
667
- if not os.path.exists(path_join_video_audio):
668
-
669
- # Set codec to None if not defined in class
670
- #if not hasattr(self, 'codec'):
671
- # self.codec = None
672
-
673
- # Join the video with audio segments
674
- join_audios(
675
- video_path=self.downloaded_video[0].get('path'),
676
- audio_tracks=self.downloaded_audio,
677
- out_path=path_join_video_audio,
678
- codec=self.codec
679
- )
680
-
681
- else:
682
- console.log("[red]Output join video and audio already exists.")
683
-
684
- return path_join_video_audio
685
-
686
- def _join_video_subtitles(self, input_path):
687
- """
688
- Joins subtitles with the video.
689
-
690
- Args:
691
- input_path (str): The path to the video file to which subtitles will be added.
692
-
693
- Returns:
694
- str: The path to the video with subtitles file.
695
- """
696
- path_join_video_subtitle = os.path.join(self.path_manager.base_path, "v_s.mp4")
697
- logging.info(f"JOIN subtitle path: {path_join_video_subtitle}")
698
-
699
- # Check if the video with subtitles file already exists
700
- if not os.path.exists(path_join_video_subtitle):
701
-
702
- # Join the video with subtitles
703
- join_subtitle(
704
- input_path,
705
- self.downloaded_subtitle,
706
- path_join_video_subtitle
707
- )
708
-
709
- return path_join_video_subtitle
710
-
711
-
712
- class HLS_Downloader:
713
- def __init__(self, output_filename: str=None, m3u8_playlist: str=None, m3u8_index: str=None, is_playlist_url: bool=True, is_index_url: bool=True):
714
- """
715
- Initializes the HLS_Downloader class.
716
-
717
- Args:
718
- output_filename (str): The desired output filename for the downloaded content.
719
- m3u8_playlist (str): The URL or content of the m3u8 playlist.
720
- m3u8_index (str): The index URL for m3u8 streams.
721
- is_playlist_url (bool): Flag indicating if the m3u8_playlist is a URL.
722
- is_index_url (bool): Flag indicating if the m3u8_index is a URL.
723
- """
724
- if ((m3u8_playlist == None or m3u8_playlist == "") and output_filename is None) or ((m3u8_index == None or m3u8_index == "") and output_filename is None):
725
- logging.info(f"class 'HLS_Downloader'; call __init__(); no parameter")
726
- sys.exit(0)
727
-
728
- self.output_filename = self._generate_output_filename(output_filename, m3u8_playlist, m3u8_index)
729
- self.path_manager = PathManager(self.output_filename)
730
- self.download_tracker = DownloadTracker(self.path_manager)
731
- self.content_extractor = ContentExtractor()
732
- self.content_downloader = ContentDownloader()
733
- self.content_joiner = ContentJoiner(self.path_manager)
734
-
735
- self.m3u8_playlist = m3u8_playlist
736
- self.m3u8_index = m3u8_index
737
- self.is_playlist_url = is_playlist_url
738
- self.is_index_url = is_index_url
739
- self.expected_real_time = None
740
- self.instace_parserClass = M3U8_Parser()
741
-
742
- self.request_m3u8_playlist = None
743
- self.request_m3u8_index = None
744
- if (m3u8_playlist == None or m3u8_playlist == ""):
745
- self.request_m3u8_index = HttpClient().get(self.m3u8_index)
746
- if (m3u8_index == None or m3u8_index == ""):
747
- self.request_m3u8_playlist = HttpClient().get(self.m3u8_playlist)
748
-
749
- def _generate_output_filename(self, output_filename, m3u8_playlist, m3u8_index):
750
- """
751
- Generates a valid output filename based on provided parameters.
752
-
753
- Args:
754
- output_filename (str): The desired output filename.
755
- m3u8_playlist (str): The m3u8 playlist URL or content.
756
- m3u8_index (str): The m3u8 index URL.
757
-
758
- Returns:
759
- str: The generated output filename.
760
- """
761
- root_path = config_manager.get('DEFAULT', 'root_path')
762
- new_filename = None
763
- new_folder = os.path.join(root_path, "undefined")
764
- logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); destination folder: {new_folder}")
765
-
766
- # Auto-generate output file name if not present
767
- if (output_filename is None) or ("mp4" not in output_filename):
768
- if m3u8_playlist is not None:
769
- new_filename = os.path.join(new_folder, compute_sha1_hash(m3u8_playlist) + ".mp4")
770
- else:
771
- new_filename = os.path.join(new_folder, compute_sha1_hash(m3u8_index) + ".mp4")
772
-
773
- else:
774
-
775
- # Check if output_filename contains a folder path
776
- folder, base_name = os.path.split(output_filename)
777
-
778
- # If no folder is specified, default to 'undefined'
779
- if not folder:
780
- folder = new_folder
781
-
782
- # Sanitize base name and folder
783
- folder = os_manager.get_sanitize_path(folder)
784
- base_name = os_manager.get_sanitize_file(base_name)
785
- os_manager.create_path(folder)
786
-
787
- # Parse to only ASCII for compatibility across platforms
788
- new_filename = os.path.join(folder, base_name)
789
-
790
- logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); return path: {new_filename}")
791
- return new_filename
792
-
793
- def start(self):
794
- """
795
- Initiates the downloading process. Checks if the output file already exists and proceeds with processing the playlist or index.
796
- """
797
- if os.path.exists(self.output_filename):
798
- console.log("[red]Output file already exists.")
799
- return 400
800
-
801
- self.path_manager.create_directories()
802
-
803
- # Determine whether to process a playlist or index
804
- if self.m3u8_playlist:
805
- if self.m3u8_playlist is not None:
806
- if self.request_m3u8_playlist != 404:
807
- logging.info(f"class 'HLS_Downloader'; call start(); parse m3u8 data")
808
-
809
- self.instace_parserClass.parse_data(uri=self.m3u8_playlist, raw_content=self.request_m3u8_playlist)
810
- is_masterPlaylist = self.instace_parserClass.is_master_playlist
811
-
812
- # Check if it's a real master playlist
813
- if is_masterPlaylist:
814
- if not GET_ONLY_LINK:
815
- r_proc = self._process_playlist()
816
-
817
- if r_proc == 404:
818
- return 404
819
- else:
820
- return None
821
-
822
- else:
823
- return {
824
- 'path': self.output_filename,
825
- 'url': self.m3u8_playlist
826
- }
827
-
828
- else:
829
- console.log("[red]Error: URL passed to M3U8_Parser is an index playlist; expected a master playlist. Crucimorfo strikes again!")
830
- else:
831
- console.log(f"[red]Error: m3u8_playlist failed request for: {self.m3u8_playlist}")
832
- else:
833
- console.log("[red]Error: m3u8_playlist is None")
834
-
835
- elif self.m3u8_index:
836
- if self.m3u8_index is not None:
837
- if self.request_m3u8_index != 404:
838
- logging.info(f"class 'HLS_Downloader'; call start(); parse m3u8 data")
839
-
840
- self.instace_parserClass.parse_data(uri=self.m3u8_index, raw_content=self.request_m3u8_index)
841
- is_masterPlaylist = self.instace_parserClass.is_master_playlist
842
-
843
- # Check if it's a real index playlist
844
- if not is_masterPlaylist:
845
- if not GET_ONLY_LINK:
846
- self._process_index()
847
- return None
848
-
849
- else:
850
- return {
851
- 'path': self.output_filename,
852
- 'url': self.m3u8_index
853
- }
854
-
855
- else:
856
- console.log("[red]Error: URL passed to M3U8_Parser is an master playlist; expected a index playlist. Crucimorfo strikes again!")
857
- else:
858
- console.log("[red]Error: m3u8_index failed request")
859
- else:
860
- console.log("[red]Error: m3u8_index is None")
861
-
862
- # Reset
863
- self._reset()
864
-
865
- def _clean(self, out_path: str) -> None:
866
- """
867
- Cleans up temporary files and folders after downloading and processing.
868
-
869
- Args:
870
- out_path (str): The path of the output file to be cleaned up.
871
- """
872
-
873
- # Check if the final output file exists
874
- logging.info(f"Check if end file converted exists: {out_path}")
875
- if out_path is None or not os.path.isfile(out_path):
876
- logging.error("Video file converted does not exist.")
877
- sys.exit(0)
878
-
879
- # Rename the output file to the desired output filename if it does not already exist
880
- if not os.path.exists(self.output_filename):
881
- missing_ts = False
882
- missing_info = ""
883
-
884
- # Rename the converted file to the specified output filename
885
- os.rename(out_path, self.output_filename)
886
-
887
- # Calculate file size and duration for reporting
888
- formatted_size = internet_manager.format_file_size(os.path.getsize(self.output_filename))
889
- formatted_duration = print_duration_table(self.output_filename, description=False, return_string=True)
890
-
891
- # Collect info about type missing
892
- for item in list_MissingTs:
893
- if int(item['nFailed']) >= 1:
894
- missing_ts = True
895
- missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]\n"
896
-
897
- # Prepare the report panel content
898
- print("")
899
- panel_content = (
900
- f"[bold green]Download completed![/bold green]\n"
901
- f"[cyan]File size: [bold red]{formatted_size}[/bold red]\n"
902
- f"[cyan]Duration: [bold]{formatted_duration}[/bold]\n"
903
- f"[cyan]Output: [bold]{os.path.abspath(self.output_filename)}[/bold]"
904
- )
905
-
906
- if missing_ts:
907
- panel_content += f"\n{missing_info}"
908
-
909
- # Display the download completion message
910
- console.print(Panel(
911
- panel_content,
912
- title=f"{os.path.basename(self.output_filename.replace('.mp4', ''))}",
913
- border_style="green"
914
- ))
915
-
916
- # Handle missing segments
917
- if missing_ts:
918
- os.rename(self.output_filename, self.output_filename.replace(".mp4", "_failed.mp4"))
919
-
920
- # Delete all temporary files except for the output file
921
- os_manager.remove_files_except_one(self.path_manager.base_path, os.path.basename(self.output_filename.replace(".mp4", "_failed.mp4")))
922
-
923
- # Remove the base folder if specified
924
- if REMOVE_SEGMENTS_FOLDER:
925
- os_manager.remove_folder(self.path_manager.base_path)
926
-
927
- else:
928
- logging.info("Video file converted already exists.")
929
-
930
- def _valida_playlist(self):
931
- """
932
- Validates the m3u8 playlist content, saves it to a temporary file, and collects playlist information.
933
- """
934
- logging.info("class 'HLS_Downloader'; call _valida_playlist()")
935
-
936
- # Retrieve the m3u8 playlist content
937
- if self.is_playlist_url:
938
- if self.request_m3u8_playlist != 404:
939
- m3u8_playlist_text = self.request_m3u8_playlist
940
- m3u8_url_fixer.set_playlist(self.m3u8_playlist)
941
-
942
- else:
943
- logging.info(f"class 'HLS_Downloader'; call _process_playlist(); return 404")
944
- return 404
945
-
946
- else:
947
- m3u8_playlist_text = self.m3u8_playlist
948
-
949
- # Check if the m3u8 content is valid
950
- if m3u8_playlist_text is None:
951
- console.log("[red]Playlist m3u8 to download is empty.")
952
- sys.exit(0)
953
-
954
- # Save the m3u8 playlist text to a temporary file
955
- open(os.path.join(self.path_manager.base_temp, "playlist.m3u8"), "w+", encoding="utf-8").write(m3u8_playlist_text)
956
-
957
- # Collect information about the playlist
958
- if self.is_playlist_url:
959
- self.content_extractor.start(self.instace_parserClass)
960
- else:
961
- self.content_extractor.start("https://fake.com", m3u8_playlist_text)
962
-
963
- def _process_playlist(self):
964
- """
965
- Processes the m3u8 playlist to download video, audio, and subtitles.
966
- """
967
- self._valida_playlist()
968
-
969
- # Add downloaded elements to the tracker
970
- self.download_tracker.add_video(self.content_extractor.m3u8_index)
971
- self.download_tracker.add_audio(self.content_extractor.list_available_audio)
972
- self.download_tracker.add_subtitle(self.content_extractor.list_available_subtitles)
973
-
974
- # Download each type of content
975
- if DOWNLOAD_VIDEO and len(self.download_tracker.downloaded_video) > 0:
976
- self.content_downloader.download_video(self.download_tracker.downloaded_video)
977
- if DOWNLOAD_AUDIO and len(self.download_tracker.downloaded_audio) > 0:
978
- self.content_downloader.download_audio(self.download_tracker.downloaded_audio)
979
- if DOWNLOAD_SUBTITLE and len(self.download_tracker.downloaded_subtitle) > 0:
980
- self.content_downloader.download_subtitle(self.download_tracker.downloaded_subtitle)
981
-
982
- # Join downloaded content
983
- self.content_joiner.setup(self.download_tracker.downloaded_video, self.download_tracker.downloaded_audio, self.download_tracker.downloaded_subtitle, self.content_extractor.codec)
984
-
985
- # Clean up temporary files and directories
986
- self._clean(self.content_joiner.converted_out_path)
987
-
988
- def _process_index(self):
989
- """
990
- Processes the m3u8 index to download only video.
991
- """
992
- m3u8_url_fixer.set_playlist(self.m3u8_index)
993
-
994
- # Download video
995
- self.download_tracker.add_video(self.m3u8_index)
996
- self.content_downloader.download_video(self.download_tracker.downloaded_video)
997
-
998
- # Join video
999
- self.content_joiner.setup(self.download_tracker.downloaded_video, [], [])
1000
-
1001
- # Clean up temporary files and directories
1002
- self._clean(self.content_joiner.converted_out_path)
1003
-
1004
- def _reset(self):
1005
- global list_MissingTs, m3u8_url_fixer
1006
-
1007
- m3u8_url_fixer.reset_playlist()
1008
- list_MissingTs = []
1
+ # 17.10.24
2
+
3
+ import os
4
+ import re
5
+ import time
6
+ import logging
7
+ import shutil
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ # External libraries
12
+ import httpx
13
+
14
+
15
+ # Internal utilities
16
+ from StreamingCommunity.Util._jsonConfig import config_manager
17
+ from StreamingCommunity.Util.headers import get_headers
18
+ from StreamingCommunity.Util.console import console, Panel
19
+ from StreamingCommunity.Util.os import (
20
+ compute_sha1_hash,
21
+ os_manager,
22
+ internet_manager
23
+ )
24
+ from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance
25
+
26
+
27
+ # Logic class
28
+ from ...FFmpeg import (
29
+ print_duration_table,
30
+ join_video,
31
+ join_audios,
32
+ join_subtitle
33
+ )
34
+ from ...M3U8 import M3U8_Parser, M3U8_UrlFix
35
+ from .segments import M3U8_Segments
36
+
37
+
38
+ # Config
39
+ ENABLE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_audio')
40
+ ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
41
+ DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
42
+ DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
43
+ MERGE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_audio')
44
+ MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
45
+ CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
46
+ FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolution')
47
+ GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
48
+ RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
49
+ MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
50
+
51
+ TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
52
+
53
+
54
+
55
+ class HLSClient:
56
+ """Client for making HTTP requests to HLS endpoints with retry mechanism."""
57
+ def __init__(self):
58
+ self.headers = {'User-Agent': get_headers()}
59
+
60
+ def request(self, url: str, return_content: bool = False) -> Optional[httpx.Response]:
61
+ """
62
+ Makes HTTP GET requests with retry logic.
63
+
64
+ Args:
65
+ url: Target URL to request
66
+ return_content: If True, returns response content instead of text
67
+
68
+ Returns:
69
+ Response content/text or None if all retries fail
70
+ """
71
+ client = httpx.Client(headers=self.headers, timeout=MAX_TIMEOUT, follow_redirects=True)
72
+ for attempt in range(RETRY_LIMIT):
73
+ try:
74
+ response = client.get(url)
75
+ response.raise_for_status()
76
+ return response.content if return_content else response.text
77
+
78
+ except Exception as e:
79
+ logging.error(f"Attempt {attempt+1} failed: {str(e)}")
80
+ time.sleep(1.5 ** attempt)
81
+ return None
82
+
83
+
84
+ class PathManager:
85
+ """Manages file paths and directories for downloaded content."""
86
+ def __init__(self, m3u8_url: str, output_path: Optional[str]):
87
+ """
88
+ Args:
89
+ m3u8_url: Source M3U8 playlist URL
90
+ output_path: Desired output path for the final video file
91
+ """
92
+ self.m3u8_url = m3u8_url
93
+ self.output_path = self._sanitize_output_path(output_path)
94
+ base_name = os.path.basename(self.output_path).replace(".mp4", "")
95
+ self.temp_dir = os.path.join(os.path.dirname(self.output_path), f"{base_name}_tmp")
96
+
97
+ def _sanitize_output_path(self, path: Optional[str]) -> str:
98
+ """
99
+ Ensures output path is valid and follows expected format.
100
+ Creates a hash-based filename if no path is provided.
101
+ """
102
+ if not path:
103
+ root = config_manager.get('DEFAULT', 'root_path')
104
+ hash_name = compute_sha1_hash(self.m3u8_url) + ".mp4"
105
+ return os.path.join(root, "undefined", hash_name)
106
+
107
+ if not path.endswith(".mp4"):
108
+ path += ".mp4"
109
+
110
+ return os_manager.get_sanitize_path(path)
111
+
112
+ def setup_directories(self):
113
+ """Creates necessary directories for temporary files (video, audio, subtitles)."""
114
+ os.makedirs(self.temp_dir, exist_ok=True)
115
+ for subdir in ['video', 'audio', 'subs']:
116
+ os.makedirs(os.path.join(self.temp_dir, subdir), exist_ok=True)
117
+
118
+ def move_final_file(self, final_file: str):
119
+ """Moves the final merged file to the desired output location."""
120
+ if os.path.exists(self.output_path):
121
+ os.remove(self.output_path)
122
+ shutil.move(final_file, self.output_path)
123
+
124
+ def cleanup(self):
125
+ """Removes temporary directories if configured to do so."""
126
+ if CLEANUP_TMP:
127
+ os_manager.remove_folder(self.temp_dir)
128
+
129
+
130
+ class M3U8Manager:
131
+ """Handles M3U8 playlist parsing and stream selection."""
132
+ def __init__(self, m3u8_url: str, client: HLSClient):
133
+ self.m3u8_url = m3u8_url
134
+ self.client = client
135
+ self.parser = M3U8_Parser()
136
+ self.url_fixer = M3U8_UrlFix()
137
+ self.video_url = None
138
+ self.video_res = None
139
+ self.audio_streams = []
140
+ self.sub_streams = []
141
+ self.is_master = False
142
+
143
+ def parse(self):
144
+ """
145
+ Fetches and parses the M3U8 playlist content.
146
+ Determines if it's a master playlist (index) or media playlist.
147
+ """
148
+ content = self.client.request(self.m3u8_url)
149
+ if not content:
150
+ raise ValueError("Failed to fetch M3U8 content")
151
+
152
+ self.parser.parse_data(uri=self.m3u8_url, raw_content=content)
153
+ self.url_fixer.set_playlist(self.m3u8_url)
154
+ self.is_master = self.parser.is_master_playlist
155
+
156
+ def select_streams(self):
157
+ """
158
+ Selects video, audio, and subtitle streams based on configuration.
159
+ If it's a master playlist, only selects video stream.
160
+ """
161
+ if not self.is_master:
162
+ if FILTER_CUSTOM_REOLUTION != -1:
163
+ self.video_url, self.video_res = self.parser._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
164
+ else:
165
+ self.video_url, self.video_res = self.parser._video.get_best_uri()
166
+
167
+ self.audio_streams = []
168
+ self.sub_streams = []
169
+
170
+ else:
171
+ if FILTER_CUSTOM_REOLUTION != -1:
172
+ self.video_url, self.video_res = self.parser._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
173
+ else:
174
+ self.video_url, self.video_res = self.parser._video.get_best_uri()
175
+
176
+ self.audio_streams = []
177
+ if ENABLE_AUDIO:
178
+ self.audio_streams = [
179
+ s for s in (self.parser._audio.get_all_uris_and_names() or [])
180
+ if s.get('language') in DOWNLOAD_SPECIFIC_AUDIO
181
+ ]
182
+
183
+ self.sub_streams = []
184
+ if ENABLE_SUBTITLE:
185
+ self.sub_streams = [
186
+ s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
187
+ if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
188
+ ]
189
+
190
+ def log_selection(self):
191
+ if FILTER_CUSTOM_REOLUTION == -1:
192
+ set_resolution = "Best"
193
+ else:
194
+ set_resolution = f"{FILTER_CUSTOM_REOLUTION}p"
195
+
196
+ tuple_available_resolution = self.parser._video.get_list_resolution()
197
+ list_available_resolution = [f"{r[0]}x{r[1]}" for r in tuple_available_resolution]
198
+
199
+ console.print(
200
+ f"[cyan bold]Video →[/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
201
+ f"[red]Set:[/red] [purple]{set_resolution}[/purple] | "
202
+ f"[yellow]Downloadable:[/yellow] [purple]{self.video_res[0]}x{self.video_res[1]}[/purple]"
203
+ )
204
+
205
+ if self.parser.codec is not None:
206
+ available_codec_info = (
207
+ f"[green]v[/green]: [yellow]{self.parser.codec.video_codec_name}[/yellow] "
208
+ f"([green]b[/green]: [yellow]{self.parser.codec.video_bitrate // 1000}k[/yellow]), "
209
+ f"[green]a[/green]: [yellow]{self.parser.codec.audio_codec_name}[/yellow] "
210
+ f"([green]b[/green]: [yellow]{self.parser.codec.audio_bitrate // 1000}k[/yellow])"
211
+ )
212
+ set_codec_info = available_codec_info if config_manager.get_bool("M3U8_CONVERSION", "use_codec") else "[purple]copy[/purple]"
213
+
214
+ console.print(
215
+ f"[bold cyan]Codec →[/bold cyan] [green]Available:[/green] {available_codec_info} | "
216
+ f"[red]Set:[/red] {set_codec_info}"
217
+ )
218
+
219
+ available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
220
+ available_sub_languages = [sub.get('language') for sub in available_subtitles]
221
+ downloadable_sub_languages = list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
222
+ if available_sub_languages:
223
+ console.print(
224
+ f"[cyan bold]Subtitle →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_sub_languages)}[/purple] | "
225
+ f"[red]Set:[/red] [purple]{', '.join(DOWNLOAD_SPECIFIC_SUBTITLE)}[/purple] | "
226
+ f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_sub_languages)}[/purple]"
227
+ )
228
+
229
+ available_audio = self.parser._audio.get_all_uris_and_names() or []
230
+ available_audio_languages = [audio.get('language') for audio in available_audio]
231
+ downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
232
+ if available_audio_languages:
233
+ console.print(
234
+ f"[cyan bold]Audio →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_audio_languages)}[/purple] | "
235
+ f"[red]Set:[/red] [purple]{', '.join(DOWNLOAD_SPECIFIC_AUDIO)}[/purple] | "
236
+ f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_audio_languages)}[/purple]"
237
+ )
238
+ print("")
239
+
240
+
241
+ class DownloadManager:
242
+ """Manages downloading of video, audio, and subtitle streams."""
243
+ def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix):
244
+ """
245
+ Args:
246
+ temp_dir: Directory for storing temporary files
247
+ client: HLSClient instance for making requests
248
+ url_fixer: URL fixer instance for generating complete URLs
249
+ """
250
+ self.temp_dir = temp_dir
251
+ self.client = client
252
+ self.url_fixer = url_fixer
253
+ self.missing_segments = []
254
+ self.stopped = False
255
+
256
+ def download_video(self, video_url: str):
257
+ """Downloads video segments from the M3U8 playlist."""
258
+ video_full_url = self.url_fixer.generate_full_url(video_url)
259
+ video_tmp_dir = os.path.join(self.temp_dir, 'video')
260
+
261
+ downloader = M3U8_Segments(url=video_full_url, tmp_folder=video_tmp_dir)
262
+ result = downloader.download_streams("Video", "video")
263
+ self.missing_segments.append(result)
264
+
265
+ if result.get('stopped', False):
266
+ self.stopped = True
267
+ return self.stopped
268
+
269
+ def download_audio(self, audio: Dict):
270
+ """Downloads audio segments for a specific language track."""
271
+ if self.stopped:
272
+ return True
273
+
274
+ audio_full_url = self.url_fixer.generate_full_url(audio['uri'])
275
+ audio_tmp_dir = os.path.join(self.temp_dir, 'audio', audio['language'])
276
+
277
+ downloader = M3U8_Segments(url=audio_full_url, tmp_folder=audio_tmp_dir)
278
+ result = downloader.download_streams(f"Audio {audio['language']}", "audio")
279
+ self.missing_segments.append(result)
280
+
281
+ if result.get('stopped', False):
282
+ self.stopped = True
283
+ return self.stopped
284
+
285
+ def download_subtitle(self, sub: Dict):
286
+ """Downloads and saves subtitle file for a specific language."""
287
+ if self.stopped:
288
+ return True
289
+
290
+ content = self.client.request(sub['uri'])
291
+ if content:
292
+ sub_path = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
293
+ with open(sub_path, 'w', encoding='utf-8') as f:
294
+ f.write(content)
295
+
296
+ return self.stopped
297
+
298
+ def download_all(self, video_url: str, audio_streams: List[Dict], sub_streams: List[Dict]):
299
+ """
300
+ Downloads all selected streams (video, audio, subtitles).
301
+ """
302
+ video_file = os.path.join(self.temp_dir, 'video', '0.ts')
303
+ if not os.path.exists(video_file):
304
+ if self.download_video(video_url):
305
+ return True
306
+
307
+ for audio in audio_streams:
308
+ if self.stopped:
309
+ break
310
+
311
+ audio_file = os.path.join(self.temp_dir, 'audio', audio['language'], '0.ts')
312
+ if not os.path.exists(audio_file):
313
+ if self.download_audio(audio):
314
+ return True
315
+
316
+ for sub in sub_streams:
317
+ if self.stopped:
318
+ break
319
+
320
+ sub_file = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
321
+ if not os.path.exists(sub_file):
322
+ if self.download_subtitle(sub):
323
+ return True
324
+
325
+ return self.stopped
326
+
327
+
328
+ class MergeManager:
329
+ """Handles merging of video, audio, and subtitle streams."""
330
+ def __init__(self, temp_dir: str, parser: M3U8_Parser, audio_streams: List[Dict], sub_streams: List[Dict]):
331
+ """
332
+ Args:
333
+ temp_dir: Directory containing temporary files
334
+ parser: M3U8 parser instance with codec information
335
+ audio_streams: List of audio streams to merge
336
+ sub_streams: List of subtitle streams to merge
337
+ """
338
+ self.temp_dir = temp_dir
339
+ self.parser = parser
340
+ self.audio_streams = audio_streams
341
+ self.sub_streams = sub_streams
342
+
343
+ def merge(self) -> str:
344
+ """
345
+ Merges downloaded streams into final video file.
346
+ Returns path to the final merged file.
347
+
348
+ Process:
349
+ 1. If no audio/subs, just process video
350
+ 2. If audio exists, merge with video
351
+ 3. If subtitles exist, add them to the video
352
+ """
353
+ video_file = os.path.join(self.temp_dir, 'video', '0.ts')
354
+ merged_file = video_file
355
+
356
+ if not self.audio_streams and not self.sub_streams:
357
+ merged_file = join_video(
358
+ video_path=video_file,
359
+ out_path=os.path.join(self.temp_dir, 'video.mp4'),
360
+ codec=self.parser.codec
361
+ )
362
+
363
+ else:
364
+ if MERGE_AUDIO and self.audio_streams:
365
+ audio_tracks = [{
366
+ 'path': os.path.join(self.temp_dir, 'audio', a['language'], '0.ts'),
367
+ 'name': a['language']
368
+ } for a in self.audio_streams]
369
+
370
+ merged_audio_path = os.path.join(self.temp_dir, 'merged_audio.mp4')
371
+ merged_file = join_audios(
372
+ video_path=video_file,
373
+ audio_tracks=audio_tracks,
374
+ out_path=merged_audio_path,
375
+ codec=self.parser.codec
376
+ )
377
+
378
+ if MERGE_SUBTITLE and self.sub_streams:
379
+ sub_tracks = [{
380
+ 'path': os.path.join(self.temp_dir, 'subs', f"{s['language']}.vtt"),
381
+ 'language': s['language']
382
+ } for s in self.sub_streams]
383
+
384
+ merged_subs_path = os.path.join(self.temp_dir, 'final.mp4')
385
+ merged_file = join_subtitle(
386
+ video_path=merged_file,
387
+ subtitles_list=sub_tracks,
388
+ out_path=merged_subs_path
389
+ )
390
+
391
+ return merged_file
392
+
393
+
394
+ class HLS_Downloader:
395
+ """Main class for HLS video download and processing."""
396
+ def __init__(self, m3u8_url: str, output_path: Optional[str] = None):
397
+ self.m3u8_url = m3u8_url
398
+ self.path_manager = PathManager(m3u8_url, output_path)
399
+ self.client = HLSClient()
400
+ self.m3u8_manager = M3U8Manager(m3u8_url, self.client)
401
+ self.download_manager: Optional[DownloadManager] = None
402
+ self.merge_manager: Optional[MergeManager] = None
403
+
404
+ def start(self) -> Dict[str, Any]:
405
+ """
406
+ Main execution flow with handling for both index and playlist M3U8s.
407
+
408
+ Returns:
409
+ Dict containing:
410
+ - path: Output file path
411
+ - url: Original M3U8 URL
412
+ - is_master: Whether the M3U8 was a master playlist
413
+ Or raises an exception if there's an error
414
+ """
415
+ if TELEGRAM_BOT:
416
+ bot = get_bot_instance()
417
+
418
+ try:
419
+ if os.path.exists(self.path_manager.output_path):
420
+ console.print(f"[red]Output file {self.path_manager.output_path} already exists![/red]")
421
+ response = {
422
+ 'path': self.path_manager.output_path,
423
+ 'url': self.m3u8_url,
424
+ 'is_master': False,
425
+ 'error': 'File already exists',
426
+ 'stopped': False
427
+ }
428
+ if TELEGRAM_BOT:
429
+ bot.send_message(response)
430
+ return response
431
+
432
+ self.path_manager.setup_directories()
433
+
434
+ # Parse M3U8 and determine if it's a master playlist
435
+ self.m3u8_manager.parse()
436
+ self.m3u8_manager.select_streams()
437
+ self.m3u8_manager.log_selection()
438
+
439
+ self.download_manager = DownloadManager(
440
+ temp_dir=self.path_manager.temp_dir,
441
+ client=self.client,
442
+ url_fixer=self.m3u8_manager.url_fixer
443
+ )
444
+
445
+ # Check if download was stopped
446
+ download_stopped = self.download_manager.download_all(
447
+ video_url=self.m3u8_manager.video_url,
448
+ audio_streams=self.m3u8_manager.audio_streams,
449
+ sub_streams=self.m3u8_manager.sub_streams
450
+ )
451
+
452
+ if download_stopped:
453
+ return {
454
+ 'path': None,
455
+ 'url': self.m3u8_url,
456
+ 'is_master': self.m3u8_manager.is_master,
457
+ 'error': 'Download stopped by user',
458
+ 'stopped': True
459
+ }
460
+
461
+ self.merge_manager = MergeManager(
462
+ temp_dir=self.path_manager.temp_dir,
463
+ parser=self.m3u8_manager.parser,
464
+ audio_streams=self.m3u8_manager.audio_streams,
465
+ sub_streams=self.m3u8_manager.sub_streams
466
+ )
467
+
468
+ final_file = self.merge_manager.merge()
469
+ self.path_manager.move_final_file(final_file)
470
+ self.path_manager.cleanup()
471
+
472
+ self._print_summary()
473
+
474
+ return {
475
+ 'path': self.path_manager.output_path,
476
+ 'url': self.m3u8_url,
477
+ 'is_master': self.m3u8_manager.is_master,
478
+ 'stopped': False
479
+ }
480
+
481
+ except Exception as e:
482
+ error_msg = str(e)
483
+ console.print(f"[red]Download failed: {error_msg}[/red]")
484
+ logging.error("Download error", exc_info=True)
485
+
486
+ return {
487
+ 'path': None,
488
+ 'url': self.m3u8_url,
489
+ 'is_master': getattr(self.m3u8_manager, 'is_master', None),
490
+ 'error': error_msg,
491
+ 'stopped': False
492
+ }
493
+
494
+ def _print_summary(self):
495
+ """Prints download summary including file size, duration, and any missing segments."""
496
+ if TELEGRAM_BOT:
497
+ bot = get_bot_instance()
498
+
499
+ missing_ts = False
500
+ missing_info = ""
501
+ for item in self.download_manager.missing_segments:
502
+ if int(item['nFailed']) >= 1:
503
+ missing_ts = True
504
+ missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]\n"
505
+
506
+ file_size = internet_manager.format_file_size(os.path.getsize(self.path_manager.output_path))
507
+ duration = print_duration_table(self.path_manager.output_path, description=False, return_string=True)
508
+
509
+ print()
510
+ panel_content = (
511
+ f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
512
+ f"[cyan]Duration: [bold]{duration}[/bold]\n"
513
+ f"[cyan]Output: [bold]{os.path.abspath(self.path_manager.output_path)}[/bold]"
514
+ )
515
+
516
+ if TELEGRAM_BOT:
517
+ message = f"Download completato\nDimensione: {file_size}\nDurata: {duration}\nPercorso: {os.path.abspath(self.path_manager.output_path)}"
518
+ clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message)
519
+ bot.send_message(clean_message, None)
520
+
521
+ if missing_ts:
522
+ panel_content += f"\n{missing_info}"
523
+ os.rename(self.path_manager.output_path, self.path_manager.output_path.replace(".mp4", "_failed.mp4"))
524
+
525
+ console.print(Panel(
526
+ panel_content,
527
+ title=f"{os.path.basename(self.path_manager.output_path.replace('.mp4', ''))}",
528
+ border_style="green"
529
+ ))