Merge pull request #521 from arabcoders/dev
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled
Refactor: switch native builds to onedir
This commit is contained in:
commit
54dedb6696
11 changed files with 99 additions and 64 deletions
29
.github/workflows/main.yml
vendored
29
.github/workflows/main.yml
vendored
|
|
@ -421,42 +421,53 @@ jobs:
|
||||||
script: |
|
script: |
|
||||||
const tag = context.ref.replace('refs/tags/', '');
|
const tag = context.ref.replace('refs/tags/', '');
|
||||||
|
|
||||||
const latestRelease = await github.rest.repos.listReleases({
|
const { data: releases } = await github.rest.repos.listReleases({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
});
|
});
|
||||||
|
|
||||||
const release = latestRelease.data.find(r => r.tag_name === tag);
|
const release = releases.find(r => r.tag_name === tag);
|
||||||
if (!release) {
|
if (!release) {
|
||||||
core.setFailed(`Release with tag ${tag} not found`);
|
core.setFailed(`Release with tag ${tag} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseTag = releases[1]?.tag_name || '';
|
||||||
|
if (!baseTag) {
|
||||||
|
core.setFailed('Could not determine a base tag (no previous release found)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { data: comparison } = await github.rest.repos.compareCommits({
|
const { data: comparison } = await github.rest.repos.compareCommits({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
base: latestRelease.data[1]?.tag_name || '',
|
base: baseTag,
|
||||||
head: tag,
|
head: tag,
|
||||||
});
|
});
|
||||||
|
|
||||||
const commits = comparison.commits.filter(c => 1 === c.parents.length);
|
const commits = comparison.commits.filter(c => c.parents.length === 1);
|
||||||
|
|
||||||
const changelog = commits.map(
|
const changelog = commits
|
||||||
c => `- ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]}`
|
.map(c => `- ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]}`)
|
||||||
).join('\n');
|
.join('\n');
|
||||||
|
|
||||||
if (!changelog) {
|
if (!changelog) {
|
||||||
core.setFailed('No commits found for the changelog');
|
core.setFailed('No commits found for the changelog');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Changelog for ${tag}:\n${changelog}`);
|
const existingBody = (release.body || '').trim();
|
||||||
|
|
||||||
|
const separator = `\n\n---\n\n## Commits since ${baseTag}\n\n`;
|
||||||
|
const newBody = existingBody
|
||||||
|
? `${existingBody}${separator}${changelog}`
|
||||||
|
: `## Commits since ${baseTag}\n\n${changelog}`;
|
||||||
|
|
||||||
await github.rest.repos.updateRelease({
|
await github.rest.repos.updateRelease({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
release_id: release.id,
|
release_id: release.id,
|
||||||
body: changelog
|
body: newBody,
|
||||||
});
|
});
|
||||||
|
|
||||||
dockerhub-sync-readme:
|
dockerhub-sync-readme:
|
||||||
|
|
|
||||||
43
.github/workflows/native-build.yml
vendored
43
.github/workflows/native-build.yml
vendored
|
|
@ -60,6 +60,7 @@ jobs:
|
||||||
|
|
||||||
- name: Replace app.spec with version from workflow branch
|
- name: Replace app.spec with version from workflow branch
|
||||||
if: ${{ github.event.inputs.useWorkflowSpec == 'true' }}
|
if: ${{ github.event.inputs.useWorkflowSpec == 'true' }}
|
||||||
|
shell: bash
|
||||||
run: cp temp-spec/app.spec ./app.spec
|
run: cp temp-spec/app.spec ./app.spec
|
||||||
|
|
||||||
- name: Cache Python venv
|
- name: Cache Python venv
|
||||||
|
|
@ -78,6 +79,7 @@ jobs:
|
||||||
architecture: "x64"
|
architecture: "x64"
|
||||||
|
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pip install uv
|
pip install uv
|
||||||
uv venv --system-site-packages --relocatable
|
uv venv --system-site-packages --relocatable
|
||||||
|
|
@ -97,18 +99,34 @@ jobs:
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
working-directory: ui
|
working-directory: ui
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm install --production --prefer-offline --frozen-lockfile
|
pnpm install --production --prefer-offline --frozen-lockfile
|
||||||
pnpm run generate
|
pnpm run generate
|
||||||
|
|
||||||
|
- name: Clean build outputs (Unix)
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
shell: bash
|
||||||
|
run: rm -rf build dist release || true
|
||||||
|
|
||||||
|
- name: Clean build outputs (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
if (Test-Path build) { Remove-Item -Recurse -Force build }
|
||||||
|
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
|
||||||
|
if (Test-Path release) { Remove-Item -Recurse -Force release }
|
||||||
|
|
||||||
- name: Build native binary (Windows)
|
- name: Build native binary (Windows)
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
.\.venv\Scripts\activate
|
.\.venv\Scripts\activate
|
||||||
uv run pyinstaller ./app.spec
|
uv run pyinstaller ./app.spec
|
||||||
|
|
||||||
- name: Build native binary (Linux/macOS)
|
- name: Build native binary (Linux/macOS)
|
||||||
if: matrix.os != 'windows-latest'
|
if: matrix.os != 'windows-latest'
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
uv run pyinstaller ./app.spec
|
uv run pyinstaller ./app.spec
|
||||||
|
|
@ -117,34 +135,45 @@ jobs:
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}
|
name: app-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}
|
||||||
path: dist/*
|
path: dist/YTPTube/**
|
||||||
|
|
||||||
- name: Package artifact (Unix)
|
- name: Package artifact (Unix)
|
||||||
if: startsWith(env.TAG_NAME, 'v') && runner.os != 'Windows'
|
if: startsWith(env.TAG_NAME, 'v') && runner.os != 'Windows'
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release
|
mkdir -p release
|
||||||
|
PKG_DIR="YTPTube-${{ env.TAG_NAME }}"
|
||||||
ZIP_NAME="ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip"
|
ZIP_NAME="ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip"
|
||||||
|
|
||||||
if [ ! -d dist ] || [ -z "$(ls -A dist)" ]; then
|
if [ ! -f "dist/YTPTube/YTPTube" ]; then
|
||||||
echo "dist directory is empty, skipping packaging."
|
echo "Missing dist/YTPTube/YTPTube"
|
||||||
|
find dist -maxdepth 3 -type f | head -n 200
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
zip -r "release/${ZIP_NAME}" dist/
|
rm -rf "dist/${PKG_DIR}"
|
||||||
|
mv "dist/YTPTube" "dist/${PKG_DIR}"
|
||||||
|
|
||||||
|
zip -r "release/${ZIP_NAME}" "dist/${PKG_DIR}"
|
||||||
|
|
||||||
- name: Package artifact (Windows)
|
- name: Package artifact (Windows)
|
||||||
if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows'
|
if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
|
$pkgDir = "YTPTube-${{ env.TAG_NAME }}"
|
||||||
$zipName = "ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip"
|
$zipName = "ytptube-${{ runner.os }}-${{ matrix.arch }}-${{ env.TAG_NAME }}.zip"
|
||||||
New-Item -ItemType Directory -Force -Path release | Out-Null
|
New-Item -ItemType Directory -Force -Path release | Out-Null
|
||||||
|
|
||||||
if (!(Test-Path dist) -or !(Get-ChildItem -Path dist)) {
|
if (!(Test-Path "dist\YTPTube\YTPTube.exe")) {
|
||||||
Write-Host "dist directory is empty, skipping packaging."
|
Write-Host "Missing dist\YTPTube\YTPTube.exe"
|
||||||
|
Get-ChildItem -Recurse dist | Select-Object -First 200 | Format-Table -AutoSize
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Compress-Archive -Path dist\* -DestinationPath "release\$zipName"
|
if (Test-Path "dist\$pkgDir") { Remove-Item -Recurse -Force "dist\$pkgDir" }
|
||||||
|
Move-Item "dist\YTPTube" "dist\$pkgDir"
|
||||||
|
|
||||||
|
Compress-Archive -Path "dist\$pkgDir" -DestinationPath "release\$zipName" -Force
|
||||||
|
|
||||||
- name: Upload to GitHub Release
|
- name: Upload to GitHub Release
|
||||||
if: startsWith(env.TAG_NAME, 'v')
|
if: startsWith(env.TAG_NAME, 'v')
|
||||||
|
|
|
||||||
40
app.spec
40
app.spec
|
|
@ -13,10 +13,6 @@ MANUAL_MAP = {
|
||||||
|
|
||||||
|
|
||||||
def top_level_modules(dist_name: str) -> list[str]:
|
def top_level_modules(dist_name: str) -> list[str]:
|
||||||
"""
|
|
||||||
Resolve distribution name -> importable top-level modules.
|
|
||||||
Honors MANUAL_MAP first; falls back to metadata or normalized name.
|
|
||||||
"""
|
|
||||||
manual: list[str] | None = MANUAL_MAP.get(dist_name) or MANUAL_MAP.get(dist_name.replace("-", "_"))
|
manual: list[str] | None = MANUAL_MAP.get(dist_name) or MANUAL_MAP.get(dist_name.replace("-", "_"))
|
||||||
if manual:
|
if manual:
|
||||||
return manual
|
return manual
|
||||||
|
|
@ -24,7 +20,7 @@ def top_level_modules(dist_name: str) -> list[str]:
|
||||||
try:
|
try:
|
||||||
dist = importlib.metadata.distribution(dist_name)
|
dist = importlib.metadata.distribution(dist_name)
|
||||||
|
|
||||||
top_level = []
|
top_level: list[str] = []
|
||||||
for f in dist.files or []:
|
for f in dist.files or []:
|
||||||
if f.name == "top_level.txt":
|
if f.name == "top_level.txt":
|
||||||
txt = (dist.locate_file(f)).read_text().splitlines()
|
txt = (dist.locate_file(f)).read_text().splitlines()
|
||||||
|
|
@ -44,7 +40,7 @@ def top_level_modules(dist_name: str) -> list[str]:
|
||||||
return [dist_name.replace("-", "_")]
|
return [dist_name.replace("-", "_")]
|
||||||
|
|
||||||
|
|
||||||
def parse_pyproject(path="pyproject.toml") -> set[str]:
|
def parse_pyproject(path: str = "pyproject.toml") -> set[str]:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
data = tomllib.load(f)
|
data = tomllib.load(f)
|
||||||
|
|
||||||
|
|
@ -52,12 +48,11 @@ def parse_pyproject(path="pyproject.toml") -> set[str]:
|
||||||
for extra in data["project"].get("optional-dependencies", {}).values():
|
for extra in data["project"].get("optional-dependencies", {}).values():
|
||||||
reqs.update(extra)
|
reqs.update(extra)
|
||||||
|
|
||||||
# Strip env markers and versions
|
|
||||||
return {re.split(r"[ ;<>=]", r, 1)[0] for r in reqs} # noqa: B034
|
return {re.split(r"[ ;<>=]", r, 1)[0] for r in reqs} # noqa: B034
|
||||||
|
|
||||||
|
|
||||||
dist_names = parse_pyproject()
|
dist_names = parse_pyproject()
|
||||||
hidden = []
|
hidden: list[str] = []
|
||||||
|
|
||||||
for dist in dist_names:
|
for dist in dist_names:
|
||||||
if sys.platform != "win32" and dist in ("python-magic-bin", "tzdata"):
|
if sys.platform != "win32" and dist in ("python-magic-bin", "tzdata"):
|
||||||
|
|
@ -69,14 +64,13 @@ for dist in dist_names:
|
||||||
hidden += [
|
hidden += [
|
||||||
"engineio.async_drivers.aiohttp",
|
"engineio.async_drivers.aiohttp",
|
||||||
"socketio.async_drivers.aiohttp",
|
"socketio.async_drivers.aiohttp",
|
||||||
"app",
|
"socketio", # python-socketio top-level
|
||||||
"dotenv"
|
"engineio",
|
||||||
"socketio",
|
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
"engineio"
|
"dotenv",
|
||||||
|
"app",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Deduplicate
|
|
||||||
hidden = sorted(set(hidden))
|
hidden = sorted(set(hidden))
|
||||||
|
|
||||||
a = Analysis( # noqa: F821 # type: ignore
|
a = Analysis( # noqa: F821 # type: ignore
|
||||||
|
|
@ -90,23 +84,29 @@ a = Analysis( # noqa: F821 # type: ignore
|
||||||
hiddenimports=hidden,
|
hiddenimports=hidden,
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=["libstdc++.so.6"],
|
excludes=[], # do NOT exclude libstdc++.so.6
|
||||||
cipher=block_cipher,
|
cipher=block_cipher,
|
||||||
)
|
)
|
||||||
|
|
||||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) # type: ignore # noqa: F821
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) # type: ignore # noqa: F821
|
||||||
|
|
||||||
exe = EXE( # type: ignore # noqa: F821
|
exe = EXE( # noqa: F821 # type: ignore
|
||||||
pyz,
|
pyz,
|
||||||
a.scripts,
|
a.scripts,
|
||||||
a.binaries,
|
|
||||||
a.zipfiles,
|
|
||||||
a.datas,
|
|
||||||
name="YTPTube",
|
name="YTPTube",
|
||||||
debug=False,
|
debug=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
upx=True,
|
|
||||||
console=True,
|
console=True,
|
||||||
icon="ui/public/favicon.ico",
|
icon="ui/public/favicon.ico",
|
||||||
onefile=True,
|
exclude_binaries=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
coll = COLLECT( # noqa: F821 # type: ignore
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=False,
|
||||||
|
name="YTPTube",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ class Download:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'."
|
f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'."
|
||||||
)
|
)
|
||||||
params["cookiefile"] = str(create_cookies_file(self.info.cookies, cookie_file).as_posix())
|
params["cookiefile"] = str(create_cookies_file(self.info.cookies, cookie_file))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg: str = f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'."
|
err_msg: str = f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'."
|
||||||
self.logger.error(err_msg)
|
self.logger.error(err_msg)
|
||||||
|
|
|
||||||
|
|
@ -756,7 +756,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
if item.cookies:
|
if item.cookies:
|
||||||
try:
|
try:
|
||||||
yt_conf["cookiefile"] = str(create_cookies_file(item.cookies, cookie_file).as_posix())
|
yt_conf["cookiefile"] = str(create_cookies_file(item.cookies, cookie_file))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = f"Failed to create cookie file for '{item.url}'. '{e!s}'."
|
msg = f"Failed to create cookie file for '{item.url}'. '{e!s}'."
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
|
|
|
||||||
|
|
@ -19,34 +19,34 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [
|
||||||
"id": "3e163c6c-64eb-4448-924f-814b629b3810",
|
"id": "3e163c6c-64eb-4448-924f-814b629b3810",
|
||||||
"name": "default",
|
"name": "default",
|
||||||
"default": True,
|
"default": True,
|
||||||
"cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log",
|
"cli": "--socket-timeout 30 --download-archive %(archive_file)s",
|
||||||
"description": "Default preset for yt-dlp. It will download whatever yt-dlp decides is the best quality for the video and audio.",
|
"description": "Default preset for yt-dlp. It will download whatever yt-dlp decides is the best quality for the video and audio.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48d",
|
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48d",
|
||||||
"name": "Mobile",
|
"name": "Mobile",
|
||||||
"cli": '--socket-timeout 30 --download-archive %(config_path)s/archive.log\n-t mp4 --merge-output-format mp4 --add-chapters --remux-video mp4 \n--embed-metadata --embed-thumbnail \n--postprocessor-args "-movflags +faststart"',
|
"cli": '--socket-timeout 30 --download-archive %(archive_file)s\n-t mp4 --merge-output-format mp4 --add-chapters --remux-video mp4 \n--embed-metadata --embed-thumbnail \n--postprocessor-args "-movflags +faststart"',
|
||||||
"default": True,
|
"default": True,
|
||||||
"description": "This preset is designed for mobile devices. It will download the best quality video and audio in mp4 format, merge them, and add chapters, metadata, and thumbnail.",
|
"description": "This preset is designed for mobile devices. It will download the best quality video and audio in mp4 format, merge them, and add chapters, metadata, and thumbnail.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b",
|
"id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b",
|
||||||
"name": "1080p",
|
"name": "1080p",
|
||||||
"cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n-S vcodec:h264 --format 'bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'",
|
"cli": "--socket-timeout 30 --download-archive %(archive_file)s\n-S vcodec:h264 --format 'bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'",
|
||||||
"default": True,
|
"default": True,
|
||||||
"description": "Download the best quality video and audio in mp4 format for 1080p resolution.",
|
"description": "Download the best quality video and audio in mp4 format for 1080p resolution.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e",
|
"id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e",
|
||||||
"name": "720p",
|
"name": "720p",
|
||||||
"cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n-S vcodec:h264 --format 'bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'",
|
"cli": "--socket-timeout 30 --download-archive %(archive_file)s\n-S vcodec:h264 --format 'bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'",
|
||||||
"default": True,
|
"default": True,
|
||||||
"description": "Download the best quality video and audio in mp4 format for 720p resolution.",
|
"description": "Download the best quality video and audio in mp4 format for 720p resolution.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330",
|
"id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330",
|
||||||
"name": "Audio Only",
|
"name": "Audio Only",
|
||||||
"cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--extract-audio --add-chapters --embed-metadata --embed-thumbnail --format 'bestaudio/best'",
|
"cli": "--socket-timeout 30 --download-archive %(archive_file)s\n--extract-audio --add-chapters --embed-metadata --embed-thumbnail --format 'bestaudio/best'",
|
||||||
"default": True,
|
"default": True,
|
||||||
"description": "This preset is designed to download only the audio of the video. It will extract the audio, add chapters, metadata, and thumbnail.",
|
"description": "This preset is designed to download only the audio of the video. It will extract the audio, add chapters, metadata, and thumbnail.",
|
||||||
},
|
},
|
||||||
|
|
@ -55,7 +55,7 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [
|
||||||
"name": "Info Reader Plugin",
|
"name": "Info Reader Plugin",
|
||||||
"description": 'This preset generate specific filename format and metadata to work with yt-dlp info reader plugins for jellyfin/emby and plex and to make play state sync work for WatchState.\n\nThere is one more step you need to do via Other > Terminal if you have it enabled or directly from container shell\n\nyt-dlp -I0 --write-info-json --write-thumbnail --convert-thumbnails jpg --paths /downloads/youtube -o "%(channel|Unknown_title)s [%(channel_id|Unknown_id)s]/%(title).180B [%(channel_id|Unknown_id)s].%(ext)s" -- https://www.youtube.com/channel/UCClfFsWcT3N2I7VTXXyt84A\n\nChange the url to the channel you want to download.\n\nFor more information please visit \nhttps://github.com/arabcoders/watchstate/blob/master/FAQ.md#how-to-get-watchstate-working-with-youtube-contentlibrary',
|
"description": 'This preset generate specific filename format and metadata to work with yt-dlp info reader plugins for jellyfin/emby and plex and to make play state sync work for WatchState.\n\nThere is one more step you need to do via Other > Terminal if you have it enabled or directly from container shell\n\nyt-dlp -I0 --write-info-json --write-thumbnail --convert-thumbnails jpg --paths /downloads/youtube -o "%(channel|Unknown_title)s [%(channel_id|Unknown_id)s]/%(title).180B [%(channel_id|Unknown_id)s].%(ext)s" -- https://www.youtube.com/channel/UCClfFsWcT3N2I7VTXXyt84A\n\nChange the url to the channel you want to download.\n\nFor more information please visit \nhttps://github.com/arabcoders/watchstate/blob/master/FAQ.md#how-to-get-watchstate-working-with-youtube-contentlibrary',
|
||||||
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/%(release_date>%Y%m%d,upload_date>%Y%m%d)s - %(title).180B [%(id)s].%(ext)s",
|
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/%(release_date>%Y%m%d,upload_date>%Y%m%d)s - %(title).180B [%(id)s].%(ext)s",
|
||||||
"cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--windows-filenames --write-info-json --embed-info-json \n--convert-thumbnails jpg --write-thumbnail --embed-metadata",
|
"cli": "--socket-timeout 30 --download-archive %(archive_file)s\n--windows-filenames --write-info-json --embed-info-json \n--convert-thumbnails jpg --write-thumbnail --embed-metadata",
|
||||||
"default": True,
|
"default": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -63,7 +63,7 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [
|
||||||
"name": "NFO Maker TV",
|
"name": "NFO Maker TV",
|
||||||
"description": "This preset generate specific filename format and metadata to work with media servers like jellyfin/emby and plex for episodes.",
|
"description": "This preset generate specific filename format and metadata to work with media servers like jellyfin/emby and plex for episodes.",
|
||||||
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/S%(release_date>%Y,upload_date>%Y)sE%(release_date>%m%d,upload_date>%m%d)s - %(title).100B [%(id)s].%(ext)s",
|
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/S%(release_date>%Y,upload_date>%Y)sE%(release_date>%m%d,upload_date>%m%d)s - %(title).100B [%(id)s].%(ext)s",
|
||||||
"cli": '--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--use-postprocessor "NFOMakerPP:when=after_move" \n--windows-filenames --convert-thumbnails jpg --write-thumbnail',
|
"cli": '--socket-timeout 30 --download-archive %(archive_file)s\n--use-postprocessor "NFOMakerPP:when=after_move" \n--windows-filenames --convert-thumbnails jpg --write-thumbnail',
|
||||||
"default": True,
|
"default": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -71,7 +71,7 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [
|
||||||
"name": "NFO Maker Movie",
|
"name": "NFO Maker Movie",
|
||||||
"description": "This preset generate specific filename format and metadata to work with media servers like jellyfin/emby and plex for movies.",
|
"description": "This preset generate specific filename format and metadata to work with media servers like jellyfin/emby and plex for movies.",
|
||||||
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/S%(release_date>%Y,upload_date>%Y)sE%(release_date>%m%d,upload_date>%m%d)s - %(title).100B [%(id)s].%(ext)s",
|
"template": "%(channel)s [%(channel_id|Unknown_id)s]/Season %(release_date>%Y,upload_date>%Y|Unknown)s/S%(release_date>%Y,upload_date>%Y)sE%(release_date>%m%d,upload_date>%m%d)s - %(title).100B [%(id)s].%(ext)s",
|
||||||
"cli": '--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--use-postprocessor "NFOMakerPP:when=after_move;mode=movie" \n--windows-filenames --convert-thumbnails jpg --write-thumbnail',
|
"cli": '--socket-timeout 30 --download-archive %(archive_file)s\n--use-postprocessor "NFOMakerPP:when=after_move;mode=movie" \n--windows-filenames --convert-thumbnails jpg --write-thumbnail',
|
||||||
"default": True,
|
"default": True,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -528,7 +528,7 @@ class Config(metaclass=Singleton):
|
||||||
dict[str, str]: The replacer variables.
|
dict[str, str]: The replacer variables.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
keys: tuple[str] = ("download_path", "temp_path", "config_path")
|
keys: tuple[str] = ("download_path", "temp_path", "config_path", "archive_file")
|
||||||
return {k: getattr(self, k) for k in keys}
|
return {k: getattr(self, k) for k in keys}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,6 @@ def preload_static(root_path: Path, config: Config) -> None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
uri_path: str = f"/{file.relative_to(static_dir).as_posix()!s}"
|
uri_path: str = f"/{file.relative_to(static_dir).as_posix()!s}"
|
||||||
# uri_path: str = f"/{str(file.as_posix()).replace(f'{static_dir.as_posix()!s}/', '')}"
|
|
||||||
contentType = EXT_TO_MIME.get(file.suffix)
|
contentType = EXT_TO_MIME.get(file.suffix)
|
||||||
if not contentType:
|
if not contentType:
|
||||||
contentType = MIME.from_file(str(file))
|
contentType = MIME.from_file(str(file))
|
||||||
|
|
|
||||||
|
|
@ -596,5 +596,5 @@ async def stream_zip_download(request: Request, config: Config, cache: Cache) ->
|
||||||
LOG.info("Download cancelled by client.")
|
LOG.info("Download cancelled by client.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Streaming zip download error. {type(e).__name__}: {e}")
|
LOG.error(f"Streaming zip download error. {type(e).__name__}: {e}")
|
||||||
finally:
|
|
||||||
return response # noqa: B012
|
return response
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import time
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
from aiohttp.web_runner import GracefulExit
|
|
||||||
|
|
||||||
from app.library.config import Config
|
from app.library.config import Config
|
||||||
from app.library.DownloadQueue import DownloadQueue
|
from app.library.DownloadQueue import DownloadQueue
|
||||||
|
|
@ -118,7 +117,6 @@ async def shutdown_system(request: Request, config: Config, encoder: Encoder, no
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
await app.shutdown()
|
await app.shutdown()
|
||||||
await app.cleanup()
|
await app.cleanup()
|
||||||
raise GracefulExit
|
|
||||||
|
|
||||||
# Schedule shutdown after response
|
# Schedule shutdown after response
|
||||||
asyncio.create_task(do_shutdown())
|
asyncio.create_task(do_shutdown())
|
||||||
|
|
|
||||||
|
|
@ -243,27 +243,25 @@ const addInProgress = ref<boolean>(false)
|
||||||
const showExtras = ref<boolean>(false)
|
const showExtras = ref<boolean>(false)
|
||||||
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
||||||
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
|
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
|
||||||
|
|
||||||
const sortByNewest = (items: StoreItem[]): StoreItem[] => items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
|
|
||||||
const sortByOldest = (items: StoreItem[]): StoreItem[] => items.slice().sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0))
|
|
||||||
|
|
||||||
const queueItems = computed<StoreItem[]>(() => sortByNewest(Object.values(queue.value ?? {})))
|
|
||||||
const historyEntries = computed<StoreItem[]>(() => sortByOldest(Object.values(history.value ?? {})))
|
|
||||||
const paginationInfo = computed(() => stateStore.getPagination())
|
const paginationInfo = computed(() => stateStore.getPagination())
|
||||||
|
|
||||||
|
const queueItems = computed<StoreItem[]>(() => Object.values(queue.value ?? {}).slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)))
|
||||||
|
|
||||||
|
const historyEntries = computed<StoreItem[]>(() => {
|
||||||
|
const items = Object.values(history.value ?? {})
|
||||||
|
return items.slice().sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime())
|
||||||
|
})
|
||||||
|
|
||||||
const downloadingStatuses: ReadonlySet<ItemStatus | null> = new Set(['downloading', 'postprocessing', 'preparing'])
|
const downloadingStatuses: ReadonlySet<ItemStatus | null> = new Set(['downloading', 'postprocessing', 'preparing'])
|
||||||
|
|
||||||
const isDownloading = (status: ItemStatus | null): boolean => downloadingStatuses.has(status)
|
const isDownloading = (status: ItemStatus | null): boolean => downloadingStatuses.has(status)
|
||||||
|
|
||||||
const downloadingItems = computed<StoreItem[]>(() => queueItems.value.filter(item => isDownloading(item.status)))
|
|
||||||
const queuedItems = computed<StoreItem[]>(() => queueItems.value.filter(item => !isDownloading(item.status)))
|
|
||||||
|
|
||||||
type DisplayEntry = { item: StoreItem; source: 'queue' | 'history' }
|
type DisplayEntry = { item: StoreItem; source: 'queue' | 'history' }
|
||||||
|
|
||||||
const displayItems = computed<DisplayEntry[]>(() => [
|
const displayItems = computed<DisplayEntry[]>(() => [
|
||||||
...downloadingItems.value.map(item => ({ item, source: 'queue' as const })),
|
...queueItems.value.filter(item => isDownloading(item.status)).map(item => ({ item, source: 'queue' as const })),
|
||||||
...queuedItems.value.map(item => ({ item, source: 'queue' as const })),
|
...queueItems.value.filter(item => !isDownloading(item.status)).map(item => ({ item, source: 'queue' as const })),
|
||||||
...historyEntries.value.map(item => ({ item, source: 'history' as const })),
|
...historyEntries.value.map(item => ({ item, source: 'history' as const }))
|
||||||
])
|
])
|
||||||
|
|
||||||
const hasActiveQueue = computed<boolean>(() => queueItems.value.length > 0)
|
const hasActiveQueue = computed<boolean>(() => queueItems.value.length > 0)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue