10 Commits

Author SHA1 Message Date
b320127f16 docs: update README.md
All checks were successful
CI / test (push) Successful in 1m1s
2026-04-20 00:42:37 -05:00
d6190a2d0b security: harden code against command injection and path traversal
Core changes:

* enforce HTTPS URLs and remove shell usage in generate_release.py
* replace os.system calls with subprocess across the codebase
* validate external inputs (playlist names, video IDs)

Improvements and fixes:

* settings.py: fix typo (node.lineno → line_number); use isinstance() over type()
* youtube/get_app_version: improve git detection using subprocess.DEVNULL
* youtube/util.py: add cleanup helpers; use shutil.which for binary resolution

YouTube modules:

* watch.py: detect and flag HLS streams; remove unused audio_track_sources
* comments.py: return early when comments are disabled; add error handling
* local_playlist.py: validate playlist names to prevent path traversal
* subscriptions.py: replace asserts with proper error handling; validate video IDs

Cleanup:

* remove unused imports across modules (playlist, search, channel)
* reorganize package imports in youtube/**init**.py
* simplify test imports and fix cleanup_func in tests

Tests:

* tests/test_shorts.py: simplify imports
* tests/test_util.py: fix cleanup_func definition
2026-04-20 00:39:35 -05:00
155bd4df49 fix(settings): add AST compatibility for Python 3.12+
All checks were successful
CI / test (push) Successful in 1m5s
- Use `ast.Constant` as primary node for Python 3.8+
- Maintain backward compatibility with `ast.Num`, `ast.Str`, and `ast.NameConstant`
- Prevent crashes on Python 3.12 where legacy nodes were removed
- Add safe handling via `try/except AttributeError`
2026-04-19 22:41:48 -05:00
5577e9e1f2 feat(channels): fix pagination for "Sorted by newest - no shorts"
Some checks failed
CI / test (push) Has been cancelled
Replace UU-uploads playlist workaround (proto field 104) with direct
requests to the channel Videos tab API (tab="videos"), aligning with
Invidious content-type handling. This restores proper continuation
tokens and stable pagination (~30 videos per page).

Update display logic:
- Show channel total upload count as an upper-bound while continuation
  tokens exist.
- On final page, display exact fetched video count.
- Ensure page number never falls below current page (fix page reset to "1").

Maintain separate handling:
- Shorts and streams tabs continue using tab-specific continuation tokens.

Add test:
- TestChannelCtokenV5::test_include_shorts_false_adds_filter

Fixes issue where channels with many Shorts (e.g., Celine Dept) showed
only a few videos and broken pagination under "no shorts" sorting.
2026-04-19 22:34:14 -05:00
3795d9e4ff fix(playlists): make playlist parsing robust against filename and formatting issues
All checks were successful
CI / test (push) Successful in 53s
- Use glob lookup to find playlist files even with trailing spaces in filenames
- Sanitize lines (strip whitespace) before JSON parsing to ignore trailing spaces/empty lines
- Handle JSONDecodeError gracefully to prevent 500 errors from corrupt entries
- Return empty list on FileNotFoundError in read_playlist instead of crashing
- Extract _find_playlist_path and _parse_playlist_lines helpers for reuse
2026-04-05 18:47:21 -05:00
3cf221a1ed minor fix 2026-04-05 18:32:29 -05:00
13a0e6ceed fix(hls): improve audio track selection and auto-detect "Original"
- Auto-select "Original" audio track by default in both native and Plyr HLS players
- Fix native HLS audio selector to use numeric indices instead of string matching
- Robustly detect "original" track by checking both `name` and `lang` attributes
- Fix audio track change handler to correctly switch between available tracks
2026-04-05 18:31:35 -05:00
e8e2aa93d6 fix(channel): fix shorts/streams pagination using continuation tokens
- Add continuation_token_cache to store ctokens between page requests
- Use cached ctoken for page 2+ instead of generating fresh tokens
- Switch shorts/streams to Next/Previous buttons (no page numbers)
- Show "N+ videos" indicator when more pages are available
- Fix UnboundLocalError when page_call was undefined for shorts/streams

The issue was that YouTube's InnerTube API requires continuation tokens
for pagination on shorts/streams tabs, but the code was generating a new
ctoken each time, always returning the same 30 videos.
2026-04-05 18:19:05 -05:00
8403e30b3a Many fixes to i18n 2026-04-05 17:43:01 -05:00
f0649be5de Add HLS support to multi-audio 2026-04-05 14:56:51 -05:00
9 changed files with 82 additions and 258 deletions

View File

@@ -1,108 +1,76 @@
# Basic init yt-local for openrc
## Basic init yt-local for openrc
## Prerequisites
1. Write `/etc/init.d/ytlocal` file.
- System with OpenRC installed and configured.
- Administrative privileges (doas or sudo).
- `ytlocal` script located at `/usr/sbin/ytlocal` and application files in an accessible directory.
```
#!/sbin/openrc-run
# Distributed under the terms of the GNU General Public License v3 or later
name="yt-local"
pidfile="/var/run/ytlocal.pid"
command="/usr/sbin/ytlocal"
## Service Installation
depend() {
use net
}
1. **Create the OpenRC service script** `/etc/init.d/ytlocal`:
start_pre() {
if [ ! -f /usr/sbin/ytlocal ] ; then
eerror "Please create script file of ytlocal in '/usr/sbin/ytlocal'"
return 1
else
return 0
fi
}
```sh
#!/sbin/openrc-run
# Distributed under the terms of the GNU General Public License v3 or later
name="yt-local"
pidfile="/var/run/ytlocal.pid"
command="/usr/sbin/ytlocal"
start() {
ebegin "Starting yt-local"
start-stop-daemon --start --exec "${command}" --pidfile "${pidfile}"
eend $?
}
depend() {
use net
}
reload() {
ebegin "Reloading ${name}"
start-stop-daemon --signal HUP --pidfile "${pidfile}"
eend $?
}
start_pre() {
if [ ! -f /usr/sbin/ytlocal ]; then
eerror "Please create script file of ytlocal in '/usr/sbin/ytlocal'"
return 1
else
return 0
fi
}
stop() {
ebegin "Stopping ${name}"
start-stop-daemon --quiet --stop --exec "${command}" --pidfile "${pidfile}"
eend $?
}
```
start() {
ebegin "Starting yt-local"
start-stop-daemon --start --exec "${command}" --pidfile "${pidfile}"
eend $?
}
after, modified execute permissions:
reload() {
ebegin "Reloading ${name}"
start-stop-daemon --signal HUP --pidfile "${pidfile}"
eend $?
}
$ doas chmod a+x /etc/init.d/ytlocal
stop() {
ebegin "Stopping ${name}"
start-stop-daemon --quiet --stop --exec "${command}" --pidfile "${pidfile}"
eend $?
}
```
> [!NOTE]
> Ensure the script is executable:
>
> ```sh
> doas chmod a+x /etc/init.d/ytlocal
> ```
2. Write `/usr/sbin/ytlocal` and configure path.
2. **Create the executable script** `/usr/sbin/ytlocal`:
```
#!/usr/bin/env bash
```bash
#!/usr/bin/env bash
cd /home/your-path/ytlocal/ # change me
source venv/bin/activate
python server.py > /dev/null 2>&1 &
echo $! > /var/run/ytlocal.pid
```
# Change the working directory according to your installation path
# Example: if installed in /usr/local/ytlocal, use:
cd /home/your-path/ytlocal/ # <-- MODIFY TO YOUR PATH
source venv/bin/activate
python server.py > /dev/null 2>&1 &
echo $! > /var/run/ytlocal.pid
```
after, modified execute permissions:
> [!WARNING]
> Run this script only as root or via `doas`, as it writes to `/var/run` and uses network privileges.
$ doas chmod a+x /usr/sbin/ytlocal
> [!TIP]
> To store the PID in a different location, adjust the `pidfile` variable in the service script.
> [!IMPORTANT]
> Verify that the virtual environment (`venv`) is correctly set up and that `python` points to the appropriate version.
3. OpenRC check
> [!CAUTION]
> Do not stop the process manually; use OpenRC commands (`rc-service ytlocal stop`) to avoid race conditions.
- status: `doas rc-service ytlocal status`
- start: `doas rc-service ytlocal start`
- restart: `doas rc-service ytlocal restart`
- stop: `doas rc-service ytlocal stop`
> [!NOTE]
> When run with administrative privileges, the configuration is saved in `/root/.yt-local`, which is rootonly.
- enable: `doas rc-update add ytlocal default`
- disable: `doas rc-update del ytlocal`
## Service Management
- **Status**: `doas rc-service ytlocal status`
- **Start**: `doas rc-service ytlocal start`
- **Restart**: `doas rc-service ytlocal restart`
- **Stop**: `doas rc-service ytlocal stop`
- **Enable at boot**: `doas rc-update add ytlocal default`
- **Disable**: `doas rc-update del ytlocal`
## PostInstallation Verification
- Confirm the process is running: `doas rc-service ytlocal status`
- Inspect logs for issues: `doas tail -f /var/log/ytlocal.log` (if logging is configured).
## Troubleshooting Common Issues
- **Service fails to start**: verify script permissions, correct `command=` path, and that the virtualenv exists.
- **Port conflict**: adjust the servers port configuration before launching.
- **Import errors**: ensure all dependencies are installed in the virtual environment.
[!IMPORTANT]
Keep the service script updated when modifying startup logic or adding new dependencies.
When yt-local is run with administrator privileges,
the configuration file is stored in /root/.yt-local

View File

@@ -261,17 +261,6 @@ For security reasons, enabling this is not recommended.''',
'category': 'interface',
}),
('native_player_storyboard', {
'type': bool,
'default': False,
'label': 'Storyboard preview (native)',
'comment': '''Show thumbnail preview on hover (native player modes).
Positioning is heuristic; may misalign in Firefox/Safari.
Works best on Chromium browsers.
No effect in Plyr.''',
'category': 'interface',
}),
('use_video_download', {
'type': int,
'default': 0,

View File

@@ -9,8 +9,6 @@
--thumb-background: #222222;
--link: #00B0FF;
--link-visited: #40C4FF;
--border-color: #333333;
--thead-background: #0a0a0b;
--border-bg: #222222;
--border-bg-settings: #000000;
--border-bg-license: #000000;

View File

@@ -9,8 +9,6 @@
--thumb-background: #35404D;
--link: #22AAFF;
--link-visited: #7755FF;
--border-color: #4A5568;
--thead-background: #1a2530;
--border-bg: #FFFFFF;
--border-bg-settings: #FFFFFF;
--border-bg-license: #FFFFFF;

View File

@@ -9,8 +9,6 @@
--thumb-background: #F5F5F5;
--link: #212121;
--link-visited: #808080;
--border-color: #CCCCCC;
--thead-background: #d0d0d0;
--border-bg: #212121;
--border-bg-settings: #91918C;
--border-bg-license: #91918C;

View File

@@ -307,122 +307,18 @@ figure.sc-video {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.v-download {
grid-area: v-download;
margin-bottom: 0.5rem;
.v-download { grid-area: v-download; }
.v-download > ul.download-dropdown-content {
background: var(--secondary-background);
padding-left: 0px;
}
.v-download details {
display: block;
width: 100%;
}
.v-download > summary {
cursor: pointer;
.v-download > ul.download-dropdown-content > li.download-format {
list-style: none;
padding: 0.4rem 0;
padding-left: 1rem;
}
.v-download > summary.download-dropdown-label {
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
padding-bottom: 6px;
padding-left: .75em;
padding-right: .75em;
padding-top: 6px;
text-align: center;
white-space: nowrap;
background-color: var(--buttom);
border: 1px solid var(--button-border);
color: var(--buttom-text);
border-radius: 5px;
margin-bottom: 0.5rem;
}
.v-download > summary.download-dropdown-label:hover {
background-color: var(--buttom-hover);
}
.v-download > .download-table-container {
background: var(--secondary-background);
max-height: 65vh;
overflow-y: auto;
border: 1px solid var(--button-border);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.download-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 0.875rem;
}
.download-table thead {
background: var(--thead-background);
position: sticky;
top: 0;
z-index: 1;
}
.download-table th,
.download-table td {
padding: 0.7rem 0.9rem;
text-align: left;
border-bottom: 1px solid var(--button-border);
}
.download-table th {
font-weight: 600;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.download-table tbody tr {
transition: all 0.2s ease;
}
.download-table tbody tr:hover {
background: var(--primary-background);
}
.download-table a.download-link {
display: inline-block;
padding: 0.4rem 0.85rem;
background: rgba(0,0,0,0.12);
color: var(--buttom-text);
.v-download > ul.download-dropdown-content > li.download-format a.download-link {
text-decoration: none;
border-radius: 5px;
font-weight: 500;
font-size: 0.85rem;
transition: background 0.2s ease;
white-space: nowrap;
}
.download-table a.download-link:hover {
background: rgba(0,0,0,0.28);
color: var(--buttom-text);
}
.download-table tbody tr:last-child td {
border-bottom: none;
}
.download-table td[data-label="Ext"] {
font-family: monospace;
font-size: 0.8rem;
font-weight: 600;
}
.download-table td[data-label="Link"] {
white-space: nowrap;
vertical-align: middle;
}
.download-table td[data-label="Codecs"] {
max-width: 180px;
text-overflow: ellipsis;
overflow: hidden;
font-family: monospace;
font-size: 0.75rem;
}
.download-table td[data-label="Size"] {
font-family: monospace;
font-size: 0.85rem;
}
.download-table td[colspan="3"] {
font-style: italic;
opacity: 0.7;
}
.v-description {

View File

@@ -105,10 +105,5 @@
{% if use_dash %}
<script src="/youtube.com/static/js/av-merge.js"></script>
{% endif %}
<!-- Storyboard Preview Thumbnails (native players only; Plyr handles this internally) -->
{% if settings.use_video_player != 2 and settings.native_player_storyboard %}
<script src="/youtube.com/static/js/storyboard-preview.js"></script>
{% endif %}
</body>
</html>

View File

@@ -102,40 +102,22 @@
{% if settings.use_video_download != 0 %}
<details class="v-download">
<summary class="download-dropdown-label">{{ _('Download') }}</summary>
<div class="download-table-container">
<table class="download-table" aria-label="Download formats">
<thead>
<tr>
<th scope="col">{{ _('Ext') }}</th>
<th scope="col">{{ _('Video') }}</th>
<th scope="col">{{ _('Audio') }}</th>
<th scope="col">{{ _('Size') }}</th>
<th scope="col">{{ _('Codecs') }}</th>
<th scope="col">{{ _('Link') }}</th>
</tr>
</thead>
<tbody>
{% for format in download_formats %}
<tr>
<td data-label="{{ _('Ext') }}">{{ format['ext'] }}</td>
<td data-label="{{ _('Video') }}">{{ format['video_quality'] }}</td>
<td data-label="{{ _('Audio') }}">{{ format['audio_quality'] }}</td>
<td data-label="{{ _('Size') }}">{{ format['file_size'] }}</td>
<td data-label="{{ _('Codecs') }}">{{ format['codecs'] }}</td>
<td data-label="{{ _('Link') }}"><a class="download-link" href="{{ format['url'] }}" download="{{ title }}.{{ format['ext'] }}" aria-label="{{ _('Download') }} {{ format['ext'] }} {{ format['video_quality'] }} {{ format['audio_quality'] }}">{{ _('Download') }}</a></td>
</tr>
{% endfor %}
{% for download in other_downloads %}
<tr>
<td data-label="{{ _('Ext') }}">{{ download['ext'] }}</td>
<td data-label="{{ _('Video') }}" colspan="3">{{ download['label'] }}</td>
<td data-label="{{ _('Codecs') }}">{{ download.get('codecs', 'N/A') }}</td>
<td data-label="{{ _('Link') }}"><a class="download-link" href="{{ download['url'] }}" download aria-label="{{ _('Download') }} {{ download['label'] }}">{{ _('Download') }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<ul class="download-dropdown-content">
{% for format in download_formats %}
<li class="download-format">
<a class="download-link" href="{{ format['url'] }}" download="{{ title }}.{{ format['ext'] }}">
{{ format['ext'] }} {{ format['video_quality'] }} {{ format['audio_quality'] }} {{ format['file_size'] }} {{ format['codecs'] }}
</a>
</li>
{% endfor %}
{% for download in other_downloads %}
<li class="download-format">
<a href="{{ download['url'] }}" download>
{{ download['ext'] }} {{ download['label'] }}
</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<span class="v-download"></span>
@@ -322,8 +304,8 @@
<!-- /plyr -->
{% endif %}
<!-- Storyboard Preview Thumbnails (native players only; Plyr handles this internally) -->
{% if settings.use_video_player != 2 and settings.native_player_storyboard %}
<!-- Storyboard Preview Thumbnails -->
{% if settings.use_video_player != 2 %}
<script src="/youtube.com/static/js/storyboard-preview.js"></script>
{% endif %}

View File

@@ -1,3 +1,3 @@
from __future__ import unicode_literals
__version__ = 'v0.5.0'
__version__ = 'v0.4.5'