Merge pull request #157 from arabcoders/dev

fix yt-dlp Impersonate target and webhook not firing
This commit is contained in:
Abdulmohsen 2025-01-02 19:16:26 +03:00 committed by GitHub
commit 5301e134ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 417 additions and 107 deletions

2
.vscode/launch.json vendored
View file

@ -30,6 +30,7 @@
"console": "internalConsole",
"justMyCode": true,
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
"YTP_CONFIG_PATH": "${workspaceFolder}/var/config",
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
@ -39,6 +40,7 @@
"YTP_YTDL_DEBUG": "true",
"YTP_MAX_WORKERS": "2",
"YTP_IGNORE_UI": "true",
"YTP_PIP_IGNORE_UPDATES": "true",
}
},
{

View file

@ -19,6 +19,9 @@ curl_cffi = "*"
pysubs2 = "*"
regex = "*"
yt-dlp = "*"
mutagen = "*"
brotli = "*"
brotlicffi = "*"
[dev-packages]

176
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "d132a1657486171e8fddcc57ff4eb235886b74892116c54795c1d8122700e2f9"
"sha256": "cd9a56440fe36b901993ced28ee6ef8d301a5125344cda86519fb7c54ede1449"
},
"pipfile-spec": 6,
"requires": {
@ -156,6 +156,171 @@
"markers": "python_version >= '3.8'",
"version": "==0.23.1"
},
"brotli": {
"hashes": [
"sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208",
"sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48",
"sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354",
"sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419",
"sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a",
"sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128",
"sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c",
"sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088",
"sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9",
"sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a",
"sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3",
"sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757",
"sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2",
"sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438",
"sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578",
"sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b",
"sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b",
"sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68",
"sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0",
"sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d",
"sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943",
"sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd",
"sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409",
"sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28",
"sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da",
"sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50",
"sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f",
"sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0",
"sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547",
"sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180",
"sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0",
"sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d",
"sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a",
"sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb",
"sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112",
"sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc",
"sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2",
"sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265",
"sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327",
"sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95",
"sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec",
"sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd",
"sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c",
"sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38",
"sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914",
"sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0",
"sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a",
"sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7",
"sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368",
"sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c",
"sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0",
"sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f",
"sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451",
"sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f",
"sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8",
"sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e",
"sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248",
"sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c",
"sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91",
"sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724",
"sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7",
"sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966",
"sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9",
"sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97",
"sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d",
"sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5",
"sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf",
"sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac",
"sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b",
"sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951",
"sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74",
"sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648",
"sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60",
"sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c",
"sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1",
"sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8",
"sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d",
"sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc",
"sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61",
"sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460",
"sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751",
"sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9",
"sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2",
"sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0",
"sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1",
"sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474",
"sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75",
"sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5",
"sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f",
"sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2",
"sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f",
"sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb",
"sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6",
"sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9",
"sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111",
"sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2",
"sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01",
"sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467",
"sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619",
"sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf",
"sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408",
"sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579",
"sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84",
"sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7",
"sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c",
"sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284",
"sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52",
"sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b",
"sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59",
"sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752",
"sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1",
"sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80",
"sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839",
"sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0",
"sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2",
"sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3",
"sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64",
"sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089",
"sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643",
"sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b",
"sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e",
"sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985",
"sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596",
"sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2",
"sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"
],
"index": "pypi",
"version": "==1.1.0"
},
"brotlicffi": {
"hashes": [
"sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b",
"sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171",
"sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb",
"sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979",
"sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33",
"sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca",
"sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f",
"sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6",
"sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca",
"sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112",
"sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391",
"sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8",
"sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0",
"sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35",
"sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820",
"sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838",
"sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613",
"sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5",
"sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851",
"sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814",
"sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc",
"sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13",
"sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990",
"sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6",
"sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d",
"sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808",
"sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.1.0.0"
},
"caribou": {
"hashes": [
"sha256:13a66fc9ef9b1c9e9ef220876d59a44ee5eff9e579655252271b7514fc0ad787",
@ -549,6 +714,15 @@
"markers": "python_version >= '3.8'",
"version": "==6.1.0"
},
"mutagen": {
"hashes": [
"sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99",
"sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.47.0"
},
"propcache": {
"hashes": [
"sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4",

View file

@ -92,6 +92,8 @@ Certain values can be set via environment variables, using the `-e` parameter on
* __YTP_DB_FILE__: The path to the SQLite database file. Defaults to `{config_path}/ytptube.db`.
* __YTP_MANUAL_ARCHIVE__: The path to the manual archive file. Defaults to `{config_path}/manual_archive.log`.
* __YTP_UI_UPDATE_TITLE__: Whether to update the title of the page with the current stats. Defaults to `true`.
* __YTP_PIP_PACKAGES__: a space separated list of pip packages to install. Defaults to `""`, you can also use `{config_path}/pip.txt` to install the packages.
* __YTP_PIP_IGNORE_UPDATES__: Do not update the custom pip packages. Defaults to `false`.
## Running behind a reverse proxy

View file

@ -2,7 +2,7 @@ import math
import os
from urllib.parse import quote
from .Utils import calcDownloadPath, StreamingError
from .ffprobe import FFProbe
from .ffprobe import ffprobe
class M3u8:
@ -30,15 +30,14 @@ class M3u8:
raise StreamingError(f"File '{realFile}' does not exist.")
try:
ffprobe = FFProbe(realFile)
await ffprobe.run()
ff = await ffprobe(realFile)
except UnicodeDecodeError:
pass
if "duration" not in ffprobe.metadata:
if "duration" not in ff.metadata:
raise StreamingError(f"Unable to get '{realFile}' play duration.")
duration: float = float(ffprobe.metadata.get("duration"))
duration: float = float(ff.metadata.get("duration"))
m3u8 = []
@ -53,7 +52,7 @@ class M3u8:
segmentParams: dict = {}
for stream in ffprobe.streams:
for stream in ff.streams():
if stream.is_video():
if stream.codec_name not in self.ok_vcodecs:
segmentParams["vc"] = 1

View file

@ -0,0 +1,61 @@
import logging
import os
import subprocess
import sys
import importlib
from library.config import Config
LOG = logging.getLogger("package_installer")
class PackageInstaller:
"""
This class is responsible for installing and upgrading pip packages.
"""
def __init__(self, config: Config):
self.config: Config = config
def from_env(self):
return self.config.pip_packages.split() if self.config.pip_packages else []
def from_file(self, file_path):
if not os.path.exists(file_path):
return []
if os.access(file_path, os.R_OK):
with open(file_path, "r") as f:
return [pkg.strip() for pkg in f.readlines() if pkg.strip()]
else:
LOG.error(f"Could not read pip packages from '{file_path}'.")
return []
def action(self, pkg: str):
try:
importlib.import_module(pkg)
if self.config.pip_ignore_updates:
LOG.info(f"'{pkg}' is already installed. Skipping upgrades. as requested.")
return
LOG.info(f"'{pkg}' is already installed. Checking for upgrades...")
subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", pkg], check=True)
except ImportError:
LOG.info(f"'{pkg}' is not installed. Installing...")
subprocess.run([sys.executable, "-m", "pip", "install", pkg], check=True)
def check(self):
"""
Checks for user supplied pip packages and installs them if they are not already installed.
"""
pip_file = os.path.join(self.config.config_path, "pip.txt")
packages = list(set(self.from_env() + self.from_file(pip_file)))
if not packages:
return
LOG.info(f"Checking for user pip packages: {', '.join(packages)}")
for package in packages:
try:
self.action(package)
except Exception as e:
LOG.error(f"Failed to install or upgrade package '{package}'. Error message: {str(e)}")
LOG.exception(e)

View file

@ -2,10 +2,8 @@ import glob
import re
from pathlib import Path
from urllib.parse import quote
from aiohttp.web import Response
from .ffprobe import FFProbe
from .ffprobe import ffprobe
from .Subtitle import Subtitle
from .Utils import calcDownloadPath, checkId, StreamingError
@ -32,15 +30,14 @@ class Playlist:
)
try:
ffprobe = FFProbe(rFile)
await ffprobe.run()
ff = await ffprobe(rFile)
except UnicodeDecodeError:
pass
if "duration" not in ffprobe.metadata:
if "duration" not in ff.metadata:
raise StreamingError(f"Unable to get '{rFile}' duration.")
duration: float = float(ffprobe.metadata.get("duration"))
duration: float = float(ff.metadata.get("duration"))
playlist = []
playlist.append("#EXTM3U")

View file

@ -4,6 +4,7 @@ import logging
import os
import tempfile
from .ffprobe import ffprobe
from .config import Config
from .Utils import calcDownloadPath, StreamingError
@ -21,6 +22,7 @@ class Segments:
self.acodec = config.streamer_acodec
# sadly due to unforeseen circumstances, we have to convert the video for now.
self.vconvert = True
self.aconvert = True
async def stream(self, path: str, file: str) -> bytes:
realFile: str = calcDownloadPath(basePath=path, folder=file, createPath=False)
@ -28,6 +30,11 @@ class Segments:
if not os.path.exists(realFile):
raise StreamingError(f"File {realFile} does not exist.")
try:
ff = await ffprobe(realFile)
except UnicodeDecodeError:
pass
tmpDir: str = tempfile.gettempdir()
tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.md5(realFile.encode("utf-8")).hexdigest()}')
@ -57,24 +64,27 @@ class Segments:
fargs.append("-map_metadata")
fargs.append("-1")
fargs.append("-pix_fmt")
fargs.append("yuv420p")
fargs.append("-g")
fargs.append("52")
# video section.
if ff.has_video():
fargs.append("-pix_fmt")
fargs.append("yuv420p")
fargs.append("-g")
fargs.append("52")
fargs.append("-map")
fargs.append("0:v:0")
fargs.append("-strict")
fargs.append("-2")
fargs.append("-map")
fargs.append("0:v:0")
fargs.append("-strict")
fargs.append("-2")
fargs.append("-codec:v")
fargs.append(self.vcodec if self.vconvert else "copy")
fargs.append("-codec:v")
fargs.append(self.vcodec if self.vconvert else "copy")
# audio section.
fargs.append("-map")
fargs.append("0:a:0")
fargs.append("-codec:a")
fargs.append(self.acodec if self.aconvert else "copy")
if ff.has_audio():
fargs.append("-map")
fargs.append("0:a:0")
fargs.append("-codec:a")
fargs.append(self.acodec if self.aconvert else "copy")
fargs.append("-sn")

View file

@ -12,6 +12,7 @@ from functools import lru_cache
from typing import Any
import yt_dlp
from yt_dlp.networking.impersonate import ImpersonateTarget
LOG = logging.getLogger("Utils")
@ -157,7 +158,16 @@ def mergeDict(source: dict, destination: dict) -> dict:
def mergeConfig(config: dict, new_config: dict) -> dict:
"""Merge user provided config into default config"""
"""
Merge user provided config into default config
Args:
config (dict): Default config
new_config (dict): User provided config
Returns:
dict: Merged config
"""
ignored_keys: tuple = (
"cookiefile",
@ -171,7 +181,12 @@ def mergeConfig(config: dict, new_config: dict) -> dict:
if key in new_config:
del new_config[key]
return mergeDict(new_config, config)
conf = mergeDict(new_config, config)
if "impersonate" in conf:
conf["impersonate"] = ImpersonateTarget.from_str(conf["impersonate"])
return conf
def isDownloaded(archive_file: str, url: str) -> tuple[bool, dict[str | None, str | None, str | None]]:

View file

@ -29,20 +29,24 @@ class Webhooks:
LOG.info(f"Loading webhooks from '{file}'.")
from .Utils import load_file
(target, status, error) = load_file(file, list)
(targets, status, error) = load_file(file, list)
if not status:
raise Exception(f"{error}")
self.targets = target
for target in targets:
LOG.info(f"Will send '{target.get('on',['all'])}' events to '{target.get('name')}'.")
self.targets = targets
except Exception as e:
LOG.error(f"Error loading webhooks from '{file}'. '{e}'")
pass
async def send(self, event: str, item: ItemDTO) -> list[dict]:
async def send(self, event: str, item: ItemDTO | dict) -> list[dict]:
if len(self.targets) == 0:
return []
if not isinstance(item, ItemDTO):
if not isinstance(item, ItemDTO) and not isinstance(item, dict):
LOG.debug(f"Received invalid item type '{type(item)}' with event '{event}'.")
return []
tasks = []
@ -56,12 +60,13 @@ class Webhooks:
return await asyncio.gather(*tasks)
async def __send(self, event: str, target: dict, item: ItemDTO) -> dict:
async def __send(self, event: str, target: dict, item: ItemDTO | dict) -> dict:
from .config import Config
req: dict = target.get("request")
itemId = item.get("id", item.get("_id", "??"))
try:
LOG.info(f"Sending event '{event}' id '{item.id}' to '{target.get('name')}'.")
LOG.info(f"Sending event '{event}' id '{itemId}' to '{target.get('name')}'.")
async with httpx.AsyncClient() as client:
request_type = req.get("type", "json")
@ -76,17 +81,17 @@ class Webhooks:
match request_type:
case "json":
reqBody["json"] = item.__dict__
reqBody["json"] = item.__dict__ if isinstance(item, ItemDTO) else item
reqBody["headers"]["Content-Type"] = "application/json"
case _:
reqBody["data"] = item.__dict__
reqBody["data"] = item.__dict__ if isinstance(item, ItemDTO) else item
reqBody["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
response = await client.request(**reqBody)
respData = {"url": req.get("url"), "status": response.status_code, "text": response.text}
msg = f"Webhook target '{target.get('name')}' Responded to event '{event}' id '{item.id}' with status '{response.status_code}'."
msg = f"Webhook target '{target.get('name')}' Responded to event '{event}' id '{itemId}' with status '{response.status_code}'."
if Config.get_instance().debug and respData.get("text"):
msg += f" body '{respData.get('text','??')}'."
@ -94,7 +99,7 @@ class Webhooks:
return respData
except Exception as e:
LOG.error(f"Error sending event '{event}' id '{item.id}' to '{target.get('name')}'. '{e}'")
LOG.error(f"Error sending event '{event}' id '{itemId}' to '{target.get('name')}'. '{e}'")
return {"url": req.get("url"), "status": 500, "text": str(e)}
def emit(self, event, data, **kwargs):

View file

@ -56,6 +56,8 @@ class Config:
db_file: str = "{config_path}/ytptube.db"
manual_archive: str = "{config_path}/archive.manual.log"
ui_update_title: bool = True
pip_packages: str = ""
pip_ignore_updates: bool = False
# immutable config vars.
version: str = APP_VERSION
@ -146,6 +148,7 @@ class Config:
"remove_files",
"ignore_ui",
"ui_update_title",
"pip_ignore_updates",
)
_frontend_vars: tuple = (

View file

@ -9,6 +9,29 @@ import operator
import os
# parameter-less decorator
def async_lru_cache_decorator(async_function):
@functools.lru_cache
def cached_async_function(*args, **kwargs):
coroutine = async_function(*args, **kwargs)
return asyncio.ensure_future(coroutine)
return cached_async_function
# decorator with options
def async_lru_cache(*lru_cache_args, **lru_cache_kwargs):
def async_lru_cache_decorator(async_function):
@functools.lru_cache(*lru_cache_args, **lru_cache_kwargs)
def cached_async_function(*args, **kwargs):
coroutine = async_function(*args, **kwargs)
return asyncio.ensure_future(coroutine)
return cached_async_function
return async_lru_cache_decorator
class FFProbeError(Exception):
pass
@ -163,78 +186,86 @@ class FFStream:
raise FFProbeError("None integer bit_rate")
class FFProbe:
"""
FFProbe wraps the ffprobe command and pulls the data into an object form::
metadata=FFProbe('multimedia-file.mov')
"""
class FFProbeResult:
def __init__(self):
self.metadata: dict = {}
self.video: list[FFStream] = []
self.audio: list[FFStream] = []
self.subtitle: list[FFStream] = []
self.attachment: list[FFStream] = []
audio: list[FFStream] = []
attachment: list[FFStream] = []
streams: list[FFStream] = []
subtitle: list[FFStream] = []
video: list[FFStream] = []
metadata: dict = {}
path_to_video: str = ""
def get(self, key: str, default=None):
return getattr(self, key) if hasattr(self, key) else default
def __init__(self, path_to_video):
self.path_to_video = path_to_video
def streams(self) -> list[FFStream]:
"List of all streams."
return self.video + self.audio + self.subtitle + self.attachment
async def run(self):
try:
with open(os.devnull, "w") as tempf:
await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf)
except FileNotFoundError:
raise IOError("ffprobe not found.")
def has_video(self):
"Is there a video stream?"
return len(self.video) > 0
if not os.path.isfile(self.path_to_video):
raise IOError(f"No such media file '{self.path_to_video}'.")
def has_audio(self):
"Is there an audio stream?"
return len(self.audio) > 0
args = [
"-v",
"quiet",
"-of",
"json",
"-show_streams",
"-show_format",
self.path_to_video,
]
p = await asyncio.create_subprocess_exec(
"ffprobe",
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
exitCode = await p.wait()
data, err = await p.communicate()
if 0 == exitCode:
parsed: dict = json.loads(data.decode("utf-8"))
else:
raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'")
stream = False
self.streams = []
self.video = []
self.audio = []
self.subtitle = []
self.attachment = []
for stream in parsed.get("streams", []):
self.streams.append(FFStream(stream))
self.metadata = parsed.get("format", {})
for stream in self.streams:
if stream.is_audio():
self.audio.append(stream)
elif stream.is_video():
self.video.append(stream)
elif stream.is_subtitle():
self.subtitle.append(stream)
elif stream.is_attachment():
self.attachment.append(stream)
def has_subtitle(self):
"Is there a subtitle stream?"
return len(self.subtitle) > 0
def __repr__(self):
return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self))
@async_lru_cache(maxsize=512)
async def ffprobe(file: str) -> FFProbeResult:
"""
Run ffprobe on a file and return the parsed data as a dictionary.
Args:
file (str): The path to the media file.
Returns:
dict: A dictionary containing the parsed data.
"""
try:
with open(os.devnull, "w") as tempf:
await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf)
except FileNotFoundError:
raise IOError("ffprobe not found.")
if not os.path.isfile(file):
raise IOError(f"No such media file '{file}'.")
args = ["-v", "quiet", "-of", "json", "-show_streams", "-show_format", file]
p = await asyncio.create_subprocess_exec(
"ffprobe",
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
exitCode = await p.wait()
data, err = await p.communicate()
if 0 == exitCode:
parsed: dict = json.loads(data.decode("utf-8"))
else:
raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'")
result = FFProbeResult()
result.metadata = parsed.get("format", {})
for stream in parsed.get("streams", []):
stream = FFStream(stream)
if stream.is_audio():
result.audio.append(stream)
elif stream.is_video():
result.video.append(stream)
elif stream.is_subtitle():
result.subtitle.append(stream)
elif stream.is_attachment():
result.attachment.append(stream)
return result

View file

@ -19,6 +19,7 @@ from library.encoder import Encoder
from library.HttpAPI import HttpAPI, LOG as http_logger
from library.HttpSocket import HttpSocket
from library.Webhooks import Webhooks
from library.PackageInstaller import PackageInstaller
LOG = logging.getLogger("app")
MIME = magic.Magic(mime=True)
@ -37,6 +38,13 @@ class Main:
self.encoder = Encoder()
self.checkFolders()
try:
PackageInstaller(self.config).check()
except Exception as e:
LOG.error(f"Failed to check for packages. Error message '{str(e)}'.")
LOG.exception(e)
caribou.upgrade(self.config.db_file, os.path.join(self.rootPath, "migrations"))
connection = sqlite3.connect(database=self.config.db_file, isolation_level=None)