Merge pull request #176 from arabcoders/dev

This commit is contained in:
Abdulmohsen 2025-01-19 18:27:31 +03:00 committed by GitHub
commit bcf5593523
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 141 additions and 60 deletions

View file

@ -92,7 +92,7 @@ jobs:
cache-to: type=gha, scope=${{ github.workflow }} cache-to: type=gha, scope=${{ github.workflow }}
- name: Version tag - name: Version tag
if: github.event_name != 'pull_request' if: github.event_name == 'disabled_now'
uses: arabcoders/action-python-autotagger@master uses: arabcoders/action-python-autotagger@master
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@ -102,7 +102,7 @@ jobs:
dockerhub-sync-readme: dockerhub-sync-readme:
needs: push-build needs: push-build
if: github.event_name != 'pull_request' if: github.event_name == 'push'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Sync README - name: Sync README
@ -116,3 +116,72 @@ jobs:
with: with:
entrypoint: node entrypoint: node
args: /opt/docker-readme-sync/sync args: /opt/docker-readme-sync/sync
create_release:
needs: push-build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && github.event_name == 'push' && success()
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # so we can git log the full history
- name: Get commits between old and new
id: commits
run: |
PREVIOUS_SHA="${{ github.event.before }}"
CURRENT_SHA="${{ github.event.after }}"
echo "Previous SHA: $PREVIOUS_SHA"
echo "Current SHA: $CURRENT_SHA"
# If "before" is empty or all zeros, log all commits
if [ -z "$PREVIOUS_SHA" ] || [ "$PREVIOUS_SHA" = "0000000000000000000000000000000000000000" ]; then
LOG=$(git log --pretty=format:"- %h %s by %an")
else
LOG=$(git log "$PREVIOUS_SHA".."$CURRENT_SHA" --pretty=format:"- %h %s by %an")
fi
echo "LOG<<EOF" >> "$GITHUB_ENV"
echo "$LOG" >> "$GITHUB_ENV"
echo "EOF" >> "$GITHUB_ENV"
- name: Create local tag matching Docker's raw pattern
id: createtag
run: |
# We'll replicate raw format: branch + base_ref + date + short sha
# If base_ref is empty (normal push to master, no PR?), it won't appear.
BRANCH_NAME="${{ github.ref_name }}"
BASE_REF="${{ github.base_ref }}"
CURRENT_SHA="${{ github.event.after }}"
DATE=$(date +%Y%m%d)
SHORT_SHA=$(echo "$CURRENT_SHA" | cut -c1-7)
TAG="${BRANCH_NAME}${BASE_REF}-${DATE}-${SHORT_SHA}"
echo "Using tag: $TAG"
git config user.name "github-actions"
git config user.email "github-actions@github.com"
# Create a lightweight local tag
git tag "$TAG" "$CURRENT_SHA"
# Push the tag back to GitHub
git push origin "$TAG"
# Expose as an output
echo "TAG=$TAG" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@master
with:
tag_name: ${{ steps.createtag.outputs.TAG }}
name: "${{ steps.createtag.outputs.TAG }}"
body: ${{ env.LOG }}
draft: false
prerelease: false
generate_release_notes: true
append_body: true
make_latest: true
token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -95,13 +95,13 @@ class Download:
def _progress_hook(self, data: dict): def _progress_hook(self, data: dict):
dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields} dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields}
if "finished" == data.get("status", None) and data.get("info_dict", {}).get("filename", False): if "finished" == data.get("status", None) and data.get("info_dict", {}).get("filename", None):
dataDict["filename"] = data["info_dict"]["filename"] dataDict["filename"] = data["info_dict"]["filename"]
self.status_queue.put({"id": self.id, **dataDict}) self.status_queue.put({"id": self.id, **dataDict})
def _postprocessor_hook(self, data: dict): def _postprocessor_hook(self, data: dict):
if data.get("postprocessor") != "MoveFiles" or data.get("status") != "finished": if "MoveFiles" != data.get("postprocessor") or "finished" != data.get("status"):
return return
if self.debug: if self.debug:
@ -116,24 +116,22 @@ class Download:
def _download(self): def _download(self):
try: try:
params: dict = { params: dict = get_opts(self.preset, mergeConfig(self.default_ytdl_opts, self.ytdl_opts))
"color": "no_color", params.update(
"paths": { {
"home": self.download_dir, "color": "no_color",
"temp": self.tempPath, "paths": {"home": self.download_dir, "temp": self.tempPath},
}, "outtmpl": {"default": self.output_template, "chapter": self.output_template_chapter},
"outtmpl": {"default": self.output_template, "chapter": self.output_template_chapter}, "noprogress": True,
"noprogress": True, "break_on_existing": True,
"break_on_existing": True, "progress_hooks": [self._progress_hook],
"progress_hooks": [self._progress_hook], "postprocessor_hooks": [self._postprocessor_hook],
"postprocessor_hooks": [self._postprocessor_hook], "ignoreerrors": False,
**get_opts(self.preset, mergeConfig(self.default_ytdl_opts, self.ytdl_opts)), }
} )
if "format" not in params and self.default_ytdl_opts.get("format", None): if "format" not in params and self.default_ytdl_opts.get("format", None):
params["format"] = self.default_ytdl_opts["format"] params["format"] = "best"
params["ignoreerrors"] = False
if self.debug: if self.debug:
params["verbose"] = True params["verbose"] = True
@ -151,7 +149,7 @@ class Download:
params["cookiefile"] = f.name params["cookiefile"] = f.name
except ValueError as e: except ValueError as e:
LOG.error(f"Invalid cookies: was provided for {self.info.title} - {str(e)}") LOG.error(f"Invalid cookies: was provided for '{self.info.title}'. '{str(e)}'.")
if self.is_live or self.is_manifestless: if self.is_live or self.is_manifestless:
hasDeletedOptions = False hasDeletedOptions = False
@ -176,7 +174,7 @@ class Download:
cls = yt_dlp.YoutubeDL(params=params) cls = yt_dlp.YoutubeDL(params=params)
if isinstance(self.info_dict, dict) and len(self.info_dict) > 1: if isinstance(self.info_dict, dict) and len(self.info_dict) > 1:
LOG.debug("Downloading using pre-info.") LOG.debug(f"Downloading '{self.info.url}' using pre-info.")
cls.process_ie_result(self.info_dict, download=True) cls.process_ie_result(self.info_dict, download=True)
ret = cls._download_retcode ret = cls._download_retcode
else: else:
@ -297,7 +295,7 @@ class Download:
if self.info.status != "finished" and self.is_live: if self.info.status != "finished" and self.is_live:
LOG.warning( LOG.warning(
f"Keeping live temp folder [{self.tempPath}], as the reported status is not finished [{self.info.status}]." f"Keeping live temp folder '{self.tempPath}', as the reported status is not finished '{self.info.status}'."
) )
return return
@ -310,7 +308,7 @@ class Download:
) )
return return
LOG.info(f"Deleting Temp folder: {self.tempPath}") LOG.info(f"Deleting Temp folder '{self.tempPath}'.")
shutil.rmtree(self.tempPath, ignore_errors=True) shutil.rmtree(self.tempPath, ignore_errors=True)
async def progress_update(self): async def progress_update(self):

View file

@ -80,8 +80,8 @@ class DownloadQueue:
live_in: str | None = None live_in: str | None = None
eventType = entry.get("_type") or "video" eventType = entry.get("_type") or "video"
if eventType == "playlist": if "playlist" == eventType:
entries = entry["entries"] entries = entry.get("entries", [])
playlist_index_digits = len(str(len(entries))) playlist_index_digits = len(str(len(entries)))
results = [] results = []
for i, etr in enumerate(entries, start=1): for i, etr in enumerate(entries, start=1):
@ -106,16 +106,16 @@ class DownloadQueue:
) )
) )
if any(res["status"] == "error" for res in results): if any("error" == res["status"] for res in results):
return { return {
"status": "error", "status": "error",
"msg": ", ".join(res["msg"] for res in results if res["status"] == "error" and "msg" in res), "msg": ", ".join(res["msg"] for res in results if res["status"] == "error" and "msg" in res),
} }
return {"status": "ok"} return {"status": "ok"}
elif (eventType == "video" or eventType.startswith("url")) and "id" in entry and "title" in entry: elif ("video" == eventType or eventType.startswith("url")) and "id" in entry and "title" in entry:
# check if the video is live stream. # check if the video is live stream.
if "live_status" in entry and entry.get("live_status") == "is_upcoming": if "live_status" in entry and "is_upcoming" == entry.get("live_status"):
if "release_timestamp" in entry and entry.get("release_timestamp"): if "release_timestamp" in entry and entry.get("release_timestamp"):
live_in = formatdate(entry.get("release_timestamp")) live_in = formatdate(entry.get("release_timestamp"))
else: else:
@ -230,10 +230,12 @@ class DownloadQueue:
already=None, already=None,
): ):
ytdlp_config = ytdlp_config if ytdlp_config else {} ytdlp_config = ytdlp_config if ytdlp_config else {}
folder = str(folder) folder = str(folder) if folder else ""
filePath = calcDownloadPath(basePath=self.config.download_path, folder=folder)
LOG.info( LOG.info(
f"Adding url '{url}' to folder '{folder}' with the following options 'Preset: {preset}' 'Naming: {output_template}', 'Cookies: {ytdlp_cookies}' 'YTConfig: {ytdlp_config}'." f"Adding 'URL: {url}' to 'Folder: {filePath}' with 'Preset: {preset}' 'Naming: {output_template}', 'Cookies: {ytdlp_cookies}' 'YTConfig: {ytdlp_config}'."
) )
if isinstance(ytdlp_config, str): if isinstance(ytdlp_config, str):
@ -279,13 +281,13 @@ class DownloadQueue:
f"extract_info: for 'URL: {url}' is done in '{time.perf_counter() - started}'. Length: '{len(entry)}'." f"extract_info: for 'URL: {url}' is done in '{time.perf_counter() - started}'. Length: '{len(entry)}'."
) )
except yt_dlp.utils.ExistingVideoReached as exc: except yt_dlp.utils.ExistingVideoReached as exc:
LOG.error(f"Video has been downloaded already and recorded in archive.log file. {str(exc)}") LOG.error(f"Video has been downloaded already and recorded in archive.log file. '{str(exc)}'.")
return {"status": "error", "msg": "Video has been downloaded already and recorded in archive.log file."} return {"status": "error", "msg": "Video has been downloaded already and recorded in archive.log file."}
except yt_dlp.utils.YoutubeDLError as exc: except yt_dlp.utils.YoutubeDLError as exc:
LOG.error(f"YoutubeDLError: Unable to extract info. {str(exc)}") LOG.error(f"YoutubeDLError: Unable to extract info. '{str(exc)}'.")
return {"status": "error", "msg": str(exc)} return {"status": "error", "msg": str(exc)}
except asyncio.exceptions.TimeoutError as exc: except asyncio.exceptions.TimeoutError as exc:
LOG.error(f"TimeoutError: Unable to extract info. {str(exc)}") LOG.error(f"TimeoutError: Unable to extract info. '{str(exc)}'.")
return { return {
"status": "error", "status": "error",
"msg": f"TimeoutError: {self.config.extract_info_timeout}s reached Unable to extract info.", "msg": f"TimeoutError: {self.config.extract_info_timeout}s reached Unable to extract info.",
@ -366,7 +368,7 @@ class DownloadQueue:
os.remove(realFile) os.remove(realFile)
fileDeleted = True fileDeleted = True
else: else:
LOG.warning(f"File '{filename}' does not exist.") LOG.warning(f"Failed to remove '{itemRef}' local file '{filename}'. File not found.")
except Exception as e: except Exception as e:
LOG.error(f"Unable to remove '{itemRef}' local file '{filename}'. {str(e)}") LOG.error(f"Unable to remove '{itemRef}' local file '{filename}'. {str(e)}")
@ -448,8 +450,9 @@ class DownloadQueue:
await asyncio.sleep(1) await asyncio.sleep(1)
async def __downloadFile(self, id: str, entry: Download): async def __downloadFile(self, id: str, entry: Download):
filePath = calcDownloadPath(basePath=self.config.download_path, folder=entry.info.folder)
LOG.info( LOG.info(
f"Downloading 'id: {id}', 'Title: {entry.info.title}', 'URL: {entry.info.url}' to 'Folder: {entry.info.folder}'." f"Downloading 'id: {id}', 'Title: {entry.info.title}', 'URL: {entry.info.url}' to 'Folder: {filePath}'."
) )
try: try:

View file

@ -222,24 +222,32 @@ class HttpAPI(common):
post = await request.json() post = await request.json()
url: str = post.get("url") url: str = post.get("url")
preset: str = post.get("preset", "default")
if not url: if not url:
return web.json_response(data={"error": "url is required."}, status=web.HTTPBadRequest.status_code) return web.json_response(data={"error": "url is required."}, status=web.HTTPBadRequest.status_code)
folder: str = post.get("folder") preset: str = str(post.get("preset", self.config.default_preset))
ytdlp_cookies: str = post.get("ytdlp_cookies") folder: str = str(post.get("folder")) if post.get("folder") else ""
ytdlp_config: dict | None = post.get("ytdlp_config") ytdlp_cookies: str = str(post.get("ytdlp_cookies")) if post.get("ytdlp_cookies") else ""
output_template: str = post.get("output_template") output_template: str = str(post.get("output_template")) if post.get("output_template") else ""
if ytdlp_config is None:
ytdlp_config = {} ytdlp_config = post.get("ytdlp_config")
if isinstance(ytdlp_config, str) and ytdlp_config:
try:
ytdlp_config = json.loads(ytdlp_config)
except Exception as e:
LOG.error(f"Failed to parse json yt-dlp config for '{url}'. {str(e)}")
return web.json_response(
data={"error": f"Failed to parse json yt-dlp config. {str(e)}"},
status=web.HTTPBadRequest.status_code,
)
status = await self.add( status = await self.add(
url=url, url=url,
preset=preset, preset=preset,
folder=folder, folder=folder,
ytdlp_cookies=ytdlp_cookies, ytdlp_cookies=ytdlp_cookies,
ytdlp_config=ytdlp_config, ytdlp_config=ytdlp_config if isinstance(ytdlp_config, dict) else {},
output_template=output_template, output_template=output_template,
) )

View file

@ -1,12 +1,13 @@
import asyncio import asyncio
import errno import errno
import functools import functools
import json
import logging import logging
import os import os
import pty import pty
import shlex import shlex
from datetime import datetime
import time import time
from datetime import datetime
import socketio import socketio
from aiohttp import web from aiohttp import web
@ -161,20 +162,25 @@ class HttpSocket(common):
await self.emitter.warning("No URL provided.", to=sid) await self.emitter.warning("No URL provided.", to=sid)
return return
preset: str = str(data.get("preset", "default")) preset: str = str(data.get("preset", self.config.default_preset))
folder: str = str(data.get("folder")) folder: str = str(data.get("folder")) if data.get("folder") else ""
ytdlp_cookies: str = str(data.get("ytdlp_cookies")) ytdlp_cookies: str = str(data.get("ytdlp_cookies")) if data.get("ytdlp_cookies") else ""
ytdlp_config: dict | None = data.get("ytdlp_config") output_template: str = str(data.get("output_template")) if data.get("output_template") else ""
output_template: str = data.get("output_template")
if ytdlp_config is None: ytdlp_config = data.get("ytdlp_config")
ytdlp_config = {} if isinstance(ytdlp_config, str) and ytdlp_config:
try:
ytdlp_config = json.loads(ytdlp_config)
except Exception as e:
await self.emitter.warning(f"Failed to parse json yt-dlp config. {str(e)}", to=sid)
return
status = await self.add( status = await self.add(
url=url, url=url,
preset=preset, preset=preset,
folder=folder, folder=folder,
ytdlp_cookies=ytdlp_cookies, ytdlp_cookies=ytdlp_cookies,
ytdlp_config=ytdlp_config, ytdlp_config=ytdlp_config if isinstance(ytdlp_config, dict) else {},
output_template=output_template, output_template=output_template,
) )

View file

@ -109,7 +109,7 @@ def getVideoInfo(url: str, ytdlp_opts: dict = None, no_archive: bool = True) ->
return yt_dlp.YoutubeDL().extract_info(url, download=False) return yt_dlp.YoutubeDL().extract_info(url, download=False)
def calcDownloadPath(basePath: str, folder: str = None, createPath: bool = True) -> str: def calcDownloadPath(basePath: str, folder: str|None = None, createPath: bool = True) -> str:
"""Calculates download path and prevents folder traversal. """Calculates download path and prevents folder traversal.
Returns: Returns:

View file

@ -22,9 +22,6 @@ class common:
async def add( async def add(
self, url: str, preset: str, folder: str, ytdlp_cookies: str, ytdlp_config: dict, output_template: str self, url: str, preset: str, folder: str, ytdlp_cookies: str, ytdlp_config: dict, output_template: str
) -> dict[str, str]: ) -> dict[str, str]:
if ytdlp_config is None:
ytdlp_config = {}
if ytdlp_cookies and isinstance(ytdlp_cookies, dict): if ytdlp_cookies and isinstance(ytdlp_cookies, dict):
ytdlp_cookies = self.encoder.encode(ytdlp_cookies) ytdlp_cookies = self.encoder.encode(ytdlp_cookies)
@ -33,7 +30,7 @@ class common:
preset=preset if preset else "default", preset=preset if preset else "default",
folder=folder, folder=folder,
ytdlp_cookies=ytdlp_cookies, ytdlp_cookies=ytdlp_cookies,
ytdlp_config=ytdlp_config, ytdlp_config=ytdlp_config if isinstance(ytdlp_config, dict) else {},
output_template=output_template, output_template=output_template,
) )

View file

@ -79,8 +79,8 @@ export const useSocketStore = defineStore('socket', () => {
socket.value.on("updated", stream => { socket.value.on("updated", stream => {
const data = JSON.parse(stream); const data = JSON.parse(stream);
if (true === stateStore.has('history', item._id)) { if (true === stateStore.has('history', data._id)) {
stateStore.update('history', item._id, item); stateStore.update('history', data._id, data);
return; return;
} }