30 Commits

Author SHA1 Message Date
mani
782106e44a Merge remote-tracking branch 'upstream/master'
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
2026-01-15 00:24:50 +01:00
mani
0e0a8b853f Revert "Fix WebSocket session flapping with ForceKeepAlive protocol"
This reverts commit d5920a381e.
2026-01-14 17:02:55 +01:00
mcarlton00
8c716130a3 Merge pull request #402 from jellyfin/prepare-0.9.1
Prepare for release v0.9.1
2026-01-12 18:04:05 -05:00
jellyfin-bot
dfc9be33d7 bump version to 0.9.1 2026-01-12 23:02:26 +00:00
mcarlton00
9b592cf761 Merge pull request #401 from mcarlton00/websocket-fix
Implement proper keepalives for websocket
2026-01-12 18:01:26 -05:00
mcarlton00
d3122416cf Implement proper keepalives for websocket 2026-01-10 16:37:18 -05:00
mani
a6af9d856b Add SDH indicator to subtitle track display
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Show "SDH" (Subtitles for Deaf and Hard of Hearing) label in subtitle
selection dialog when track has IsHearingImpaired flag set.
2026-01-06 01:20:33 +01:00
mani
51aaa1f603 Remove remaining problematic module-level addon access
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Remove module-level addon variable in kodi_utils.py
- Load addon locally in add_menu_directory_item() where needed
- Load addon locally in SHOW_SETTINGS mode in functions.py
- Keep __addon__ fallback in functions.py for non-critical uses (__addondir__, addon_id, PLUGINPATH) which have safe fallback values
- loghandler.py already has proper exception handling around __pluginpath__ usage
2026-01-06 01:15:50 +01:00
mani
a71efe14c1 Properly fix intermittent installation failures
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Instead of setting addon variables to None (which causes issues),
fix the root cause by removing unnecessary module-level initialization:

- Remove module-level user_details loading in functions.py
- Load user_details locally in functions where actually needed
- Wrap remaining module-level addon access in try/except in:
  - service.py (log_timing_data)
  - default.py (log_timing_data)
  - kodi_utils.py (addon variable)
  - functions.py (__addon__ and related variables)

This prevents crashes during installation/update while avoiding
None-related issues during normal operation.
2026-01-06 01:12:59 +01:00
mani
1879a8ed01 Fix intermittent installation/update failures
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Add exception handling for addon and settings initialization during
installation/update when addon may not be fully registered yet:
- Wrap __addon__ and __pluginpath__ initialization in try/except
- Handle settings access failures in LogHandler.__init__()
- Protect formatException() and _gen_rel_path() from missing __pluginpath__
2026-01-06 01:07:20 +01:00
mani
7d6ee45263 Fix installation failure when Kodi logging is disabled
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Add exception handling in LogHandler.emit() to prevent crashes
when xbmc.log() fails due to globally disabled logging in Kodi.
2026-01-06 01:03:53 +01:00
mani
44ad891242 Change cache defaults to disabled
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Set disable_disk_cache to true by default (RAM only mode)
- Set use_cache to false by default (disable cache requests)
2026-01-06 01:01:22 +01:00
mani
a94575f83c Add audio track title to stream selection display
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Display track title in audio stream selection dialog when available.
Format: "index - language - codec layout - title"
2026-01-06 00:58:58 +01:00
mani
1c75b09824 Fix context menu implementation for track selection
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Remove incorrect addContextMenuItems() from item_functions.py as JellyCon uses custom context menu system
- Add "Play with track selection" entry to JellyCon's ActionMenu in show_menu() for Movies and Episodes
- Add handler for play_with_track_selection action that sets force_track_selection parameter
2026-01-06 00:55:13 +01:00
mani
ebd59760ae Fix context menu import causing movies not to load
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Fix incorrect import: change 'from . import translation' to
  'from .utils import translate_string'
- Add try/except block to prevent context menu errors from breaking playback
- Add defensive check for item_details.id existence
- Add error logging for debugging context menu issues

This fixes the critical bug where the movie menu would not load
due to the incorrect translation import.
2026-01-06 00:46:21 +01:00
mani
3184fa6f89 Replace subtitle codec boolean with dropdown preference
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Replace prefer_srt_over_pgs boolean with subtitle_codec_preference dropdown
  Options: 0=Prefer PGS (default), 1=Prefer SRT, 2=No preference

- More intuitive UI with descriptive labels:
  - PGS bevorzugt (bessere Qualität beim Brennen)
  - SRT bevorzugt (flexibel, kann gestreamt werden)
  - Keine Präferenz

- Update scoring logic to handle three states instead of two
- Clarify that only_forced filter applies only during auto-selection
  Manual selection via dialog still shows all subtitles
2026-01-06 00:36:25 +01:00
mani
32f2b12708 Change subtitle preferences to prefer forced-only and PGS
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Change prefer_srt_over_pgs default from true to false
  PGS has better quality when burned in during transcoding

- Add only_forced_subtitles setting (default: true)
  When enabled, only forced subtitles are selected
  Regular subtitles are skipped even if they match the language

- Skip non-forced subtitles in auto-selection when only_forced is enabled
  Prevents selecting regular German subs when watching German content
  Only shows forced subs for foreign language parts

This is useful for native language content where only foreign
language parts need subtitles (forced), not the entire movie.
2026-01-06 00:34:19 +01:00
mani
b9a6b5056a Add language-based track selection and context menu
Context Menu Integration:
- Add 'Play with track selection' option to video context menus
- Available for movies and episodes via long-press/context menu
- Forces manual track selection dialog even when auto-selection is configured

Language Selection Improvements:
- Add dropdown selectors for preferred audio/subtitle languages
- Support German, English, French, Spanish, Italian, Japanese, Russian
- Add comprehensive language matching (e.g., 'ger' matches 'deu', 'deutsch', 'german')
- Add setting to auto-select 'No subtitles' when no matching subtitle found

Track Selection Scoring System:
- Audio tracks: Language match (+100), default flag (+50),
  channel count (+5-40), codec quality (+10-30), exclude commentary (-100)
- Subtitle tracks: Language match (+100), forced flag (+50),
  SRT preference (+30), default flag (+10)

Implementation Details:
- Add force_track_selection parameter throughout playback chain
- Preserve server/remote control track selections when not forcing manual selection
- Add extensive debug logging for track selection decisions
- Respect user preferences while allowing manual override via context menu
2026-01-06 00:32:44 +01:00
mani
238d30ab94 Improve audio track selection with scoring system
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Add intelligent scoring for multiple audio tracks in same language
- Score based on: default flag (+50), channel count (+5-40),
  codec quality (+10-30), and exclude commentary (-100)
- Prevents selecting commentary or low-quality tracks when better
  options are available in the same language
- Prioritizes default track while considering quality factors
2026-01-06 00:15:46 +01:00
mani
7d26589cf6 Add automatic audio and subtitle track selection
- Add preferred audio language setting (default: ger)
- Add preferred subtitle language setting (default: ger)
- Add auto-select default audio track option
- Add prefer forced subtitles option
- Add prefer SRT over PGS/image subtitles option

Implements intelligent scoring system for subtitle selection:
- Language match: +100 points
- Forced subtitle: +50 points (if enabled)
- SRT codec: +30 points (if enabled)
- Default track: +10 points

Auto-selection only happens when no track is pre-selected.
Falls back to manual dialog selection if no match is found.
2026-01-06 00:13:17 +01:00
mani
10857e39b1 Fix incomplete transcode target codec implementation
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
- Add transcode target codec logic to get_play_url function
- Add debug logging to track codec selection
- Remove redundant enable="true" attributes in settings.xml

The get_play_url function was still using hardcoded h264, which caused
the transcode target codec setting to not take effect for direct
transcode URLs.
2026-01-05 23:41:16 +01:00
mani
30a07327bc Add option to disable disk caching (RAM only mode)
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Adds new advanced setting to disable disk-based caching, keeping data only in RAM. When enabled, prevents writing .pickle cache files and disables artwork preloading. Significantly reduces SD card wear on devices like Raspberry Pi while maintaining performance through RAM caching.
2026-01-05 18:50:26 +01:00
mani
50f2f1ac3c Add configurable transcode target video codec support
Adds new playback options to select transcode target codec (H.264, H.265/HEVC, AV1) and force H.264 transcoding. This enables optimal codec selection for client hardware, especially useful for devices with H.265 hardware decoding like Raspberry Pi 5. Target codec and source codec filtering now work independently for maximum flexibility.
2026-01-05 18:47:29 +01:00
mani
8041a0b9d8 Improve subtitle track display with codec information
Subtitle tracks now display both language and codec type (e.g., 'English (SRT)', 'German (PGS)'). Adds friendly names for common subtitle codecs like SRT, PGS, VobSub, and ASS. Improves user experience when selecting subtitle tracks.
2026-01-05 18:47:04 +01:00
mani
d5920a381e Fix WebSocket session flapping with ForceKeepAlive protocol
Implements Jellyfin's ForceKeepAlive/KeepAlive mechanism to prevent constant reconnects every 2 minutes. WebSocketApp is now recreated for each connection attempt to avoid memory leaks. Removed problematic ping_timeout and reconnect parameters that caused instability.
2026-01-05 18:46:25 +01:00
daryhanif
36b2891353 Translated using Weblate (Indonesian)
Currently translated at 98.3% (297 of 302 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/id/
2026-01-05 12:25:56 +00:00
rimasx
4fa6d7d643 Translated using Weblate (Estonian)
Currently translated at 100.0% (302 of 302 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/et/
2026-01-05 12:25:56 +00:00
rimasx
90151c2958 Translated using Weblate (Estonian)
Currently translated at 100.0% (302 of 302 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/et/
2026-01-05 08:00:55 +00:00
rimasx
4133c03759 Translated using Weblate (Estonian)
Currently translated at 100.0% (302 of 302 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/et/
2026-01-05 07:40:44 +00:00
Levente Suli
aab694c302 Translated using Weblate (Hungarian)
Currently translated at 100.0% (302 of 302 strings)

Translation: Jellycon/Jellycon
Translate-URL: https://translate.jellyfin.org/projects/jellycon/jellycon/hu/
2026-01-04 21:05:53 +00:00
19 changed files with 2670 additions and 109 deletions

144
PATCHES.md Normal file
View File

@@ -0,0 +1,144 @@
# JellyCon Patches
Diese Patches fügen neue Features und Fixes zu JellyCon hinzu.
## Patches Übersicht
### 1. websocket-keepalive-fix.patch (3.1 KB)
**Was es behebt:** Session-Flapping Problem (ständige Reconnects alle 2 Minuten)
**Änderungen:**
- Implementiert Jellyfin's ForceKeepAlive/KeepAlive Protokoll
- Entfernt problematische `ping_timeout` und `reconnect` Parameter
- WebSocketApp wird bei jedem Reconnect neu erstellt (verhindert Memory-Leaks)
- Fügt `on_close` Callback hinzu für besseres Debugging
**Betroffene Dateien:**
- `resources/lib/websocket_client.py`
**Anwendung:**
```bash
cd plugin.video.jellycon
patch -p1 < websocket-keepalive-fix.patch
```
---
### 2. transcode-target-codec.patch (4.4 KB)
**Was es hinzufügt:** Konfigurierbare Transcode-Ziel-Codecs (H.264, H.265, AV1)
**Features:**
- Neue Dropdown-Option: "Transcode target video codec"
- H.264 (default)
- H.265 (HEVC) - Perfekt für Raspberry Pi 5!
- AV1
- Neue Checkbox: "Force transcode h264"
- Unabhängige Kontrolle über Quell- und Ziel-Codecs
**Perfekt für:**
- Raspberry Pi 5 (H.265 Hardware-Decode)
- Moderne GPUs mit AV1-Unterstützung
- Optimierung der Client-Hardware-Beschleunigung
**Betroffene Dateien:**
- `resources/lib/play_utils.py`
- `resources/settings.xml`
- `resources/language/resource.language.en_gb/strings.po`
- `resources/language/resource.language.de/strings.po`
**Anwendung:**
```bash
cd plugin.video.jellycon
patch -p1 < transcode-target-codec.patch
```
**Konfiguration nach Installation:**
```
Kodi Settings → Add-ons → JellyCon → Playback
1. "Transcode target video codec" → H.265 (HEVC)
2. "Force transcode h264" → ☑ Aktivieren
3. "Force transcode h265" → ☐ Aus
Ergebnis:
- H.264 Content → Server transcodiert zu H.265 → Pi 5 Hardware-Decode
- H.265 Content → DirectPlay (Hardware!)
```
---
### 3. disable-disk-cache.patch (3.8 KB)
**Was es hinzufügt:** Option zum Deaktivieren von Disk-Caching
**Features:**
- Neue Checkbox: "Disable disk caching (RAM only)"
- Verhindert Schreiben von `.pickle` Cache-Dateien
- Deaktiviert Artwork-Preloading
- Daten bleiben nur im RAM
**Perfekt für:**
- SD-Karten (Raspberry Pi) - reduziert Schreibzyklen massiv
- Privacy - keine dauerhaften Cache-Dateien
- Immer frische Daten vom Server
**Betroffene Dateien:**
- `resources/lib/datamanager.py`
- `resources/lib/cache_images.py`
- `resources/settings.xml`
- `resources/language/resource.language.en_gb/strings.po`
- `resources/language/resource.language.de/strings.po`
**Anwendung:**
```bash
cd plugin.video.jellycon
patch -p1 < disable-disk-cache.patch
```
**Konfiguration nach Installation:**
```
Kodi Settings → Add-ons → JellyCon → Advanced
"Disable disk caching (RAM only)" → ☑ Aktivieren
```
**Hinweis:** Kodi's eigener Texture-Cache (`~/.kodi/userdata/Thumbnails/`) läuft weiterhin!
---
## Alle Patches auf einmal anwenden
```bash
cd plugin.video.jellycon
patch -p1 < websocket-keepalive-fix.patch
patch -p1 < transcode-target-codec.patch
patch -p1 < disable-disk-cache.patch
```
## Patches rückgängig machen
```bash
cd plugin.video.jellycon
patch -p1 -R < websocket-keepalive-fix.patch
patch -p1 -R < transcode-target-codec.patch
patch -p1 -R < disable-disk-cache.patch
```
## Zusammenfassung der Änderungen
```
7 Dateien geändert, 134 Zeilen hinzugefügt, 15 Zeilen entfernt
+ resources/lib/websocket_client.py (KeepAlive Fix)
+ resources/lib/play_utils.py (Transcode Target)
+ resources/lib/datamanager.py (Disk Cache Control)
+ resources/lib/cache_images.py (Disk Cache Control)
+ resources/settings.xml (Neue Settings)
+ resources/language/.../strings.po (Sprachstrings DE+EN)
```
## Getestet mit
- JellyCon Version: 0.9.0+py3
- Kodi Version: Matrix 19+
- Jellyfin Server: 10.8+
- Raspberry Pi 5 (primärer Use-Case)

View File

@@ -8,10 +8,14 @@ from resources.lib.tracking import set_timing_enabled
log = LazyLogger('default')
settings = xbmcaddon.Addon()
log_timing_data = settings.getSetting('log_timing') == "true"
if log_timing_data:
set_timing_enabled(True)
try:
settings = xbmcaddon.Addon()
log_timing_data = settings.getSetting('log_timing') == "true"
if log_timing_data:
set_timing_enabled(True)
except Exception:
# During installation/update, addon might not be fully registered yet
pass
log.debug("About to enter mainEntryPoint()")

View File

@@ -1,19 +1,8 @@
version: '0.9.0'
version: '0.9.1'
changelog: |-
:tada: New features and improvements
------------------------------------
+ Add a page limit setting for tv shows (#393) @mcarlton00
+ Add New Widgets (#381) @Aydanill
+ Include track details in audio playback (#378) @davembg
+ Add segment skip ability (#368) @Gorgorot38
Bug Fixes
---------
+ Fix websocket reconnects (#396) @manuelschneider
+ Ensure the filtered limit takes precedence over page limit (#395) @mcarlton00
+ fix subtitle burn in (#384) @loztcf
+ fix sdh/cc/hi subtitle for jellyfin > v10.9.0 (#385) @AndryYosua
+ Fix bug with multiple external subtitles in the same languages (#369) @Gorgorot38
+ Implement proper keepalives for websocket (#401) @mcarlton00
dependencies:
py2:
- addon: 'xbmc.python'

View File

@@ -1233,3 +1233,115 @@ msgstr "Alle Favoriten"
msgctxt "#30678"
msgid "TV Shows per page"
msgstr "Fernsehsendungen pro Seite"
msgctxt "#30679"
msgid "Transcode Ziel-Video-Codec"
msgstr "Transcode Ziel-Video-Codec"
msgctxt "#30680"
msgid "H.264 (Standard)"
msgstr "H.264 (Standard)"
msgctxt "#30681"
msgid "H.265 (HEVC)"
msgstr "H.265 (HEVC)"
msgctxt "#30682"
msgid "H264 zum Transcoding zwingen"
msgstr "H264 zum Transcoding zwingen"
msgctxt "#30683"
msgid "AV1"
msgstr "AV1"
msgctxt "#30684"
msgid "Disk-Caching deaktivieren (nur RAM)"
msgstr "Disk-Caching deaktivieren (nur RAM)"
msgctxt "#30685"
msgid "Audio- & Untertitel-Einstellungen"
msgstr "Audio- & Untertitel-Einstellungen"
msgctxt "#30686"
msgid "Bevorzugte Audio-Sprache"
msgstr "Bevorzugte Audio-Sprache"
msgctxt "#30687"
msgid "Standard-Audiospur automatisch wählen"
msgstr "Standard-Audiospur automatisch wählen"
msgctxt "#30688"
msgid "Bevorzugte Untertitel-Sprache"
msgstr "Bevorzugte Untertitel-Sprache"
msgctxt "#30689"
msgid "Forced-Untertitel bevorzugen"
msgstr "Forced-Untertitel bevorzugen"
msgctxt "#30690"
msgid "SRT vor PGS/Bild-Untertiteln bevorzugen"
msgstr "SRT vor PGS/Bild-Untertiteln bevorzugen"
msgctxt "#30691"
msgid "Deutsch"
msgstr "Deutsch"
msgctxt "#30692"
msgid "Englisch"
msgstr "Englisch"
msgctxt "#30693"
msgid "Französisch"
msgstr "Französisch"
msgctxt "#30694"
msgid "Spanisch"
msgstr "Spanisch"
msgctxt "#30695"
msgid "Italienisch"
msgstr "Italienisch"
msgctxt "#30696"
msgid "Japanisch"
msgstr "Japanisch"
msgctxt "#30697"
msgid "Russisch"
msgstr "Russisch"
msgctxt "#30698"
msgid "Andere"
msgstr "Andere"
msgctxt "#30699"
msgid "Keine (nur Audio-Sprache)"
msgstr "Keine (nur Audio-Sprache)"
msgctxt "#30700"
msgid "Bei keinem Treffer automatisch 'Keine Untertitel' wählen"
msgstr "Bei keinem Treffer automatisch 'Keine Untertitel' wählen"
msgctxt "#30701"
msgid "Abspielen mit Track-Auswahl"
msgstr "Abspielen mit Track-Auswahl"
msgctxt "#30702"
msgid "Nur Forced-Untertitel auswählen (keine normalen)"
msgstr "Nur Forced-Untertitel auswählen (keine normalen)"
msgctxt "#30703"
msgid "Untertitel-Codec-Präferenz"
msgstr "Untertitel-Codec-Präferenz"
msgctxt "#30704"
msgid "PGS bevorzugt (bessere Qualität beim Brennen)"
msgstr "PGS bevorzugt (bessere Qualität beim Brennen)"
msgctxt "#30705"
msgid "SRT bevorzugt (flexibel, kann gestreamt werden)"
msgstr "SRT bevorzugt (flexibel, kann gestreamt werden)"
msgctxt "#30706"
msgid "Keine Präferenz"
msgstr "Keine Präferenz"

View File

@@ -1224,3 +1224,115 @@ msgstr "Recap Skipper"
msgctxt "#30678"
msgid "TV Shows per page"
msgstr "TV Shows per page"
msgctxt "#30679"
msgid "Transcode target video codec"
msgstr "Transcode target video codec"
msgctxt "#30680"
msgid "H.264 (default)"
msgstr "H.264 (default)"
msgctxt "#30681"
msgid "H.265 (HEVC)"
msgstr "H.265 (HEVC)"
msgctxt "#30682"
msgid "Force transcode h264"
msgstr "Force transcode h264"
msgctxt "#30683"
msgid "AV1"
msgstr "AV1"
msgctxt "#30684"
msgid "Disable disk caching (RAM only)"
msgstr "Disable disk caching (RAM only)"
msgctxt "#30685"
msgid "Audio & Subtitle Preferences"
msgstr "Audio & Subtitle Preferences"
msgctxt "#30686"
msgid "Preferred audio language"
msgstr "Preferred audio language"
msgctxt "#30687"
msgid "Auto-select default audio track"
msgstr "Auto-select default audio track"
msgctxt "#30688"
msgid "Preferred subtitle language"
msgstr "Preferred subtitle language"
msgctxt "#30689"
msgid "Prefer forced subtitles"
msgstr "Prefer forced subtitles"
msgctxt "#30690"
msgid "Prefer SRT over PGS/image subtitles"
msgstr "Prefer SRT over PGS/image subtitles"
msgctxt "#30691"
msgid "German"
msgstr "German"
msgctxt "#30692"
msgid "English"
msgstr "English"
msgctxt "#30693"
msgid "French"
msgstr "French"
msgctxt "#30694"
msgid "Spanish"
msgstr "Spanish"
msgctxt "#30695"
msgid "Italian"
msgstr "Italian"
msgctxt "#30696"
msgid "Japanese"
msgstr "Japanese"
msgctxt "#30697"
msgid "Russian"
msgstr "Russian"
msgctxt "#30698"
msgid "Other"
msgstr "Other"
msgctxt "#30699"
msgid "None (audio language only)"
msgstr "None (audio language only)"
msgctxt "#30700"
msgid "Auto-select 'No subtitles' if no match found"
msgstr "Auto-select 'No subtitles' if no match found"
msgctxt "#30701"
msgid "Play with track selection"
msgstr "Play with track selection"
msgctxt "#30702"
msgid "Only select forced subtitles (no regular subs)"
msgstr "Only select forced subtitles (no regular subs)"
msgctxt "#30703"
msgid "Subtitle codec preference"
msgstr "Subtitle codec preference"
msgctxt "#30704"
msgid "Prefer PGS (better quality when burned)"
msgstr "Prefer PGS (better quality when burned)"
msgctxt "#30705"
msgid "Prefer SRT (flexible, can be streamed)"
msgstr "Prefer SRT (flexible, can be streamed)"
msgctxt "#30706"
msgid "No preference"
msgstr "No preference"

View File

@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-12-23 08:05+0000\n"
"PO-Revision-Date: 2026-01-05 12:25+0000\n"
"Last-Translator: rimasx <riks_12@hot.ee>\n"
"Language-Team: Estonian <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/et/>\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.14\n"
"X-Generator: Weblate 5.15.1\n"
msgctxt "#30021"
msgid "Show all episodes item"
@@ -73,7 +73,7 @@ msgstr "Port"
msgctxt "#30366"
msgid "Manually enter user details"
msgstr "Sisesta kasutaja andmed käsitsi"
msgstr "Sisesta kasutajaandmed käsitsi"
msgctxt "#30241"
msgid "Force transcode mpeg4"
@@ -141,11 +141,11 @@ msgstr "- Albumi esitajad"
msgctxt "#30319"
msgid "Music - All Album Artists"
msgstr "Muusika kõik albumi esitajad"
msgstr "Muusika - Kõik albumi esitajad"
msgctxt "#30352"
msgid "Music - Frequently Played"
msgstr "Muusika Sageli esitatud"
msgstr "Muusika - Sageli esitatud"
msgctxt "#30327"
msgid "Go To Season"
@@ -161,11 +161,11 @@ msgstr "Episoodid - Viimati lisatud"
msgctxt "#30257"
msgid "Movies - Recently Added"
msgstr "Filmid Viimati lisatud"
msgstr "Filmid - Viimati lisatud"
msgctxt "#30349"
msgid " - Recently Played"
msgstr "- viimati esitatud"
msgstr "- Viimati esitatud"
msgctxt "#30268"
msgid " - Recently Added"
@@ -173,11 +173,11 @@ msgstr "- Viimati lisatud"
msgctxt "#30351"
msgid "Music - Recently Played"
msgstr "Muusika viimati esitatud"
msgstr "Muusika - Viimati esitatud"
msgctxt "#30350"
msgid "Music - Recently Added"
msgstr "Muusika Viimati lisatud"
msgstr "Muusika - Viimati lisatud"
msgctxt "#30348"
msgid "Add user ratings"
@@ -245,7 +245,7 @@ msgstr "Filme lehel"
msgctxt "#30330"
msgid "Show change user dialog"
msgstr "Kuva kasutaja vahetamise dialoog"
msgstr "Kuva kasutaja vahetamise aken"
msgctxt "#30329"
msgid "Screensaver"
@@ -269,7 +269,7 @@ msgstr "- Albumid"
msgctxt "#30318"
msgid "Music - Albums"
msgstr "Muusika Albumid"
msgstr "Muusika - Albumid"
msgctxt "#30317"
msgid "Play All"
@@ -281,7 +281,7 @@ msgstr "Ühenduse viga"
msgctxt "#30315"
msgid "Suppress notifications for connection errors"
msgstr "Lülita ühenduse veateavitused välja"
msgstr "Peida ühenduse veateavitused"
msgctxt "#30314"
msgid "Play"
@@ -305,7 +305,7 @@ msgstr "Luba Jellyfini kaugjuhtimine"
msgctxt "#30309"
msgid "Select Media Source"
msgstr "Vali meedia allikas"
msgstr "Vali meediaallikas"
msgctxt "#30308"
msgid "Select Trailer"
@@ -417,7 +417,7 @@ msgstr "Värskenda puhverdatud pilte"
msgctxt "#30280"
msgid "Missing Title"
msgstr "Puuduv pealkiri"
msgstr "Puuduv nimetus"
msgctxt "#30279"
msgid "TV Shows - Unwatched"
@@ -431,7 +431,7 @@ msgctxt "#30277"
msgid "JellyCon needs to prompt for resume on partily played items, Kodi can also prompt, this can cause a double prompt. Do you want to remove the double prompt?"
msgstr ""
"JellyCon peab küsima jätkamist osaliselt esitatud üksuste puhul. Kodi võib "
"samuti küsida ja see võib põhjustada päringu. Kas soovid topeltpäringu "
"samuti küsida ja see võib põhjustada topeltpäringu. Kas soovid topeltpäringu "
"eemaldada?"
msgctxt "#30276"
@@ -460,7 +460,7 @@ msgstr "Märgi vaadatuks"
msgctxt "#30269"
msgid "Movies - Random"
msgstr "Filmid Juhuslikud"
msgstr "Filmid - Juhuslikud"
msgctxt "#30267"
msgid " - In Progress"
@@ -468,7 +468,7 @@ msgstr "- Pooleli"
msgctxt "#30266"
msgid "Movies - Pages"
msgstr "Filmid Lehed"
msgstr "Filmid - Lehed"
msgctxt "#30265"
msgid "Episodes - Next Up"
@@ -492,11 +492,11 @@ msgstr "Kogumikud"
msgctxt "#30259"
msgid "Movies - Favorites"
msgstr "Filmid Lemmikud"
msgstr "Filmid - Lemmikud"
msgctxt "#30258"
msgid "Movies - In Progress"
msgstr "Filmid Pooleli"
msgstr "Filmid - Pooleli"
msgctxt "#30256"
msgid "Movies"
@@ -508,7 +508,7 @@ msgstr "Sarjad - A-Ü"
msgctxt "#30254"
msgid "Show add-on settings"
msgstr "Kuva lisamooduli seaded"
msgstr "Lisamooduli seaded"
msgctxt "#30252"
msgid "Movies - A-Z"
@@ -516,7 +516,7 @@ msgstr "Filmid - A-Ü"
msgctxt "#30251"
msgid "Movies - Genres"
msgstr "Filmid Žanrid"
msgstr "Filmid - Žanrid"
msgctxt "#30250"
msgid "Unknown"
@@ -604,7 +604,7 @@ msgstr "HTTP otsevoog"
msgctxt "#30000"
msgid "Host"
msgstr "Serveri aadress"
msgstr "Server"
msgctxt "#30182"
msgid "Include media stream info"
@@ -676,7 +676,7 @@ msgstr "Tehtud"
msgctxt "#30121"
msgid "On resume"
msgstr "Jätkamisel"
msgstr "Taasesituse jätkamisel"
msgctxt "#30120"
msgid "Show load progress"
@@ -684,7 +684,7 @@ msgstr "Kuva laadimise edenemine"
msgctxt "#30116"
msgid "Add unwatched counts to names"
msgstr "Lisa nimedele vaatamata arv"
msgstr "Lisa nimedele vaatamata episoodide arv"
msgctxt "#30118"
msgid "Add resume percent to names"
@@ -716,7 +716,7 @@ msgstr "Hoiatus: see toiming kustutab meediafailid serverist."
msgctxt "#30091"
msgid "Confirm delete?"
msgstr "Kas kinnitada kustutamine?"
msgstr "Kas kustutamine kinnitada?"
msgctxt "#30063"
msgid "N/A"
@@ -744,7 +744,7 @@ msgstr "Luba silumislogimine"
msgctxt "#30026"
msgid "Widget item select action"
msgstr "Vidina toiming üksuse valikul"
msgstr "Toiming vidina elemendi valimisel"
msgctxt "#30025"
msgid "Password:"
@@ -768,7 +768,7 @@ msgstr "Filtreeritud episoodi nime vorming"
msgctxt "#30213"
msgid "Video force 8 bit"
msgstr "Sunni 8 bit video"
msgstr "Sunni 8 bitine video"
msgctxt "#30368"
msgid "Clear Password?"
@@ -780,7 +780,7 @@ msgstr "Kohalike serverite skaneerimine"
msgctxt "#30376"
msgid "Checking server url"
msgstr "Serveri URL-i kontrollimine"
msgstr "Serveri URL-i kontroll"
msgctxt "#30323"
msgid "Artists"
@@ -824,7 +824,7 @@ msgstr "Süsteem -"
msgctxt "#30225"
msgid "Interface Mode"
msgstr "Kasutajaliidese režiim"
msgstr "Kasutajaliidese olek"
msgctxt "#30226"
msgid "Default"
@@ -856,7 +856,7 @@ msgstr "Päringu saatmine"
msgctxt "#30375"
msgid "Receiving data packet"
msgstr "Andmepaketi vastuvõtmine"
msgstr "Andmepaketi vastuvõtt"
msgctxt "#30378"
msgid "Persist user details"
@@ -864,7 +864,7 @@ msgstr "Säilita kasutajaandmed"
msgctxt "#30379"
msgid "External subtitle prompt"
msgstr "Väliste subtiitrite pakkumine"
msgstr "Paku väliseid subtiitreid"
msgctxt "#30439"
msgid "Show play next episode at time left in seconds"
@@ -1152,7 +1152,7 @@ msgstr "Kordusvaatamise päevad (0 = keelatud)"
msgctxt "#30453"
msgid "Hide number of items to show on entry title"
msgstr "Peida pealkirjas kuvatavate üksuste arv"
msgstr "Peida kuvatavate üksuste arv avalehel"
msgctxt "#30455"
msgid "TV Shows - Random"
@@ -1173,3 +1173,51 @@ msgstr "Kõik - Lemmikud"
msgctxt "#30678"
msgid "TV Shows per page"
msgstr "Sarjade arv lehel"
msgctxt "#30020"
msgid "Flatten single season"
msgstr "Ära kuva üksikut hooaega"
msgctxt "#30224"
msgid "Interaction"
msgstr "Interaktsioon"
msgctxt "#30430"
msgid "Label"
msgstr "Silt"
msgctxt "#30438"
msgid "Play cinema intros"
msgstr "Esita filmi intro"
msgctxt "#30666"
msgid "Segment Skipper"
msgstr "Segmendi vahelejätja"
msgctxt "#30670"
msgid "Intro Skipper"
msgstr "Sissejuhatuse vahelejätja"
msgctxt "#30671"
msgid "Credit Skipper"
msgstr "Lõputiitrite vahelejätja"
msgctxt "#30675"
msgid "Commercial Skipper"
msgstr "Reklaamide vahelejätja"
msgctxt "#30676"
msgid "Preview Skipper"
msgstr "Eelvaatuse vahelejätja"
msgctxt "#30677"
msgid "Recap Skipper"
msgstr "Kokkuvõtte vahelejätja"
msgctxt "#30421"
msgid "Views"
msgstr "Vaated"
msgctxt "#30450"
msgid "Next Up Rewatching"
msgstr "Järgmisena kordusvaatamisel"

View File

@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-07-21 20:37+0000\n"
"Last-Translator: czlevi7 <czlevi7@gmail.com>\n"
"PO-Revision-Date: 2026-01-04 21:05+0000\n"
"Last-Translator: Levente Suli <suli.levente.07@icloud.com>\n"
"Language-Team: Hungarian <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/hu/>\n"
"Language: hu\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.11.4\n"
"X-Generator: Weblate 5.15.1\n"
msgctxt "#30441"
msgid "Use cached widget data"
@@ -1086,7 +1086,7 @@ msgstr "Port"
msgctxt "#30000"
msgid "Host"
msgstr "Gazda"
msgstr "Gazdagép"
msgctxt "#30444"
msgid "Login using Quick Connect"
@@ -1207,3 +1207,23 @@ msgstr "Kérdezz"
msgctxt "#30677"
msgid "Recap Skipper"
msgstr "Összefoglaló átugró"
msgctxt "#30455"
msgid "TV Shows - Random"
msgstr "TV Műsorok - Véletlenszerű"
msgctxt "#30456"
msgid "All - Random"
msgstr "Összes - Véletlenszerű"
msgctxt "#30457"
msgid "All - Recently Added"
msgstr "Összes - Nemrég hozzáadott"
msgctxt "#30458"
msgid "All - Favorites"
msgstr "Összes - Kedvencek"
msgctxt "#30678"
msgid "TV Shows per page"
msgstr "TV Műsorok oldalanként"

View File

@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-06-28 11:51+0000\n"
"Last-Translator: Nirwan <ny.unpar@gmail.com>\n"
"PO-Revision-Date: 2026-01-05 12:25+0000\n"
"Last-Translator: daryhanif <daryhanif00@gmail.com>\n"
"Language-Team: Indonesian <https://translate.jellyfin.org/projects/jellycon/"
"jellycon/id/>\n"
"Language: id\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.11.4\n"
"X-Generator: Weblate 5.15.1\n"
msgctxt "#30414"
msgid " - Favorites"
@@ -1206,3 +1206,8 @@ msgstr "Lewatkan"
msgctxt "#30677"
msgid "Recap Skipper"
msgstr "Lewati Rekap"
#, fuzzy
msgctxt "#30457"
msgid "All - Recently Added"
msgstr "Semua - Baru-Baru ini ditambahkan"

View File

@@ -41,6 +41,13 @@ class CacheArtwork(threading.Thread):
last_update = 0
home_window = HomeWindow()
settings = xbmcaddon.Addon()
# Check if disk caching is disabled
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
if disable_disk_cache:
log.debug("CacheArtwork : Disk caching disabled, artwork caching skipped")
return
latest_content_hash = "never"
check_interval = int(settings.getSetting('cacheImagesOnScreenSaver_interval'))
check_interval = check_interval * 60

View File

@@ -54,6 +54,13 @@ class DataManager:
log.debug("last_content_url : use_cache={0} url={1}".format(use_cache, url))
home_window.set_property("last_content_url", url)
# Check if disk caching is disabled
settings = xbmcaddon.Addon()
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
if disable_disk_cache:
use_cache = False
log.debug("Disk caching disabled - data will be kept in RAM only")
user_id = self.user_details.get('user_id')
server = self.api.server
@@ -164,6 +171,13 @@ class CacheManagerThread(threading.Thread):
log.debug("CacheManagerThread : Started")
home_window = HomeWindow()
settings = xbmcaddon.Addon()
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
if disable_disk_cache:
log.debug("CacheManagerThread : Disk caching disabled, skipping cache operations")
return
is_fresh = False
# if the data is fresh then just save it

View File

@@ -41,16 +41,22 @@ from .tracking import timer
from .skin_cloner import clone_default_skin
from .play_utils import play_file
__addon__ = xbmcaddon.Addon()
__addondir__ = translate_path(__addon__.getAddonInfo('profile'))
__cwd__ = __addon__.getAddonInfo('path')
PLUGINPATH = translate_path(os.path.join(__cwd__))
addon_id = __addon__.getAddonInfo('id')
try:
__addon__ = xbmcaddon.Addon()
__addondir__ = translate_path(__addon__.getAddonInfo('profile'))
__cwd__ = __addon__.getAddonInfo('path')
PLUGINPATH = translate_path(os.path.join(__cwd__))
addon_id = __addon__.getAddonInfo('id')
except Exception:
# During installation/update, addon might not be fully registered yet
__addon__ = None
__addondir__ = ''
__cwd__ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PLUGINPATH = __cwd__
addon_id = 'plugin.video.jellycon'
log = LazyLogger(__name__)
user_details = load_user_details()
@timer
def main_entry_point():
@@ -119,7 +125,8 @@ def main_entry_point():
elif mode == "CLONE_SKIN":
clone_default_skin()
elif mode == "SHOW_SETTINGS":
__addon__.openSettings()
addon = xbmcaddon.Addon()
addon.openSettings()
window = xbmcgui.getCurrentWindowId()
if window == 10000:
log.debug(
@@ -343,6 +350,12 @@ def show_menu(params):
li.setProperty('menu_id', 'play')
action_items.append(li)
# Add "Play with track selection" for Movies and Episodes
if result["Type"] in ["Episode", "Movie"]:
li = xbmcgui.ListItem(translate_string(30701), offscreen=True)
li.setProperty('menu_id', 'play_with_track_selection')
action_items.append(li)
if result["Type"] in ["Season", "MusicArtist", "MusicAlbum", "Playlist",
"MusicGenre"]:
li = xbmcgui.ListItem(translate_string(30317), offscreen=True)
@@ -473,6 +486,11 @@ def show_menu(params):
log.debug("Play Item")
play_action(params)
elif selected_action == "play_with_track_selection":
log.debug("Play Item with Track Selection")
params["force_track_selection"] = "true"
play_action(params)
elif selected_action == "set_view":
log.debug("Setting view type for {0} to {1}".format(
view_key, container_view_id)
@@ -923,6 +941,9 @@ def play_action(params):
audio_stream_index = params.get("audio_stream_index")
log.debug("audio_stream_index: {0}".format(audio_stream_index))
force_track_selection = params.get("force_track_selection", "false") == "true"
log.debug("force_track_selection: {0}".format(force_track_selection))
action = params.get("action", "play")
# set the current playing item id
@@ -939,6 +960,7 @@ def play_action(params):
play_info["media_source_id"] = media_source_id
play_info["subtitle_stream_index"] = subtitle_stream_index
play_info["audio_stream_index"] = audio_stream_index
play_info["force_track_selection"] = force_track_selection
log.info("Sending jellycon_play_action : {0}".format(play_info))
play_file(play_info)
@@ -948,7 +970,8 @@ def play_item_trailer(item_id):
handle = int(sys.argv[1]) if sys.argv and len(sys.argv) > 1 else -1
if handle != -1:
xbmcplugin.endOfDirectory(handle, succeeded=False, updateListing=False, cacheToDisc=False)
user_details = load_user_details()
url = "/Users/{}/Items/{}/LocalTrailers?format=json".format(
user_details.get('user_id'), item_id
)

View File

@@ -11,7 +11,6 @@ import xbmcaddon
from .lazylogger import LazyLogger
log = LazyLogger(__name__)
addon = xbmcaddon.Addon()
class HomeWindow:
@@ -41,6 +40,7 @@ def add_menu_directory_item(label, path, folder=True, art=None, properties=None)
li = xbmcgui.ListItem(label, path=path, offscreen=True)
if art is None:
art = {}
addon = xbmcaddon.Addon()
art["thumb"] = addon.getAddonInfo('icon')
if properties is not None:
li.setProperties(properties)

View File

@@ -12,8 +12,13 @@ from kodi_six import xbmc, xbmcaddon
from .utils import translate_path
__addon__ = xbmcaddon.Addon(id='plugin.video.jellycon')
__pluginpath__ = translate_path(__addon__.getAddonInfo('path'))
try:
__addon__ = xbmcaddon.Addon(id='plugin.video.jellycon')
__pluginpath__ = translate_path(__addon__.getAddonInfo('path'))
except Exception:
# During installation/update, addon might not be fully registered yet
__addon__ = None
__pluginpath__ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def getLogger(name=None):
@@ -32,27 +37,36 @@ class LogHandler(logging.StreamHandler):
self.sensitive = {'Token': [], 'Server': []}
settings = xbmcaddon.Addon()
self.server = settings.getSetting('server_address')
self.debug = settings.getSetting('log_debug')
try:
settings = xbmcaddon.Addon()
self.server = settings.getSetting('server_address')
self.debug = settings.getSetting('log_debug')
except Exception:
# During installation/update, settings might not be available yet
self.server = ''
self.debug = 'false'
def emit(self, record):
if self._get_log_level(record.levelno):
string = self.format(record)
try:
string = self.format(record)
# Hide server URL in logs
string = string.replace(
self.server or "{server}", "{jellyfin-server}"
)
# Hide server URL in logs
string = string.replace(
self.server or "{server}", "{jellyfin-server}"
)
py_version = sys.version_info.major
# Log level notation changed in Kodi v19
if py_version > 2:
log_level = xbmc.LOGINFO
else:
log_level = xbmc.LOGNOTICE
xbmc.log(string, level=log_level)
py_version = sys.version_info.major
# Log level notation changed in Kodi v19
if py_version > 2:
log_level = xbmc.LOGINFO
else:
log_level = xbmc.LOGNOTICE
xbmc.log(string, level=log_level)
except Exception:
# Silently fail if logging is disabled globally in Kodi
pass
def _get_log_level(self, level):
@@ -92,13 +106,16 @@ class MyFormatter(logging.Formatter):
return result
def formatException(self, exc_info):
_pluginpath_real = os.path.realpath(__pluginpath__)
try:
_pluginpath_real = os.path.realpath(__pluginpath__)
except Exception:
_pluginpath_real = None
res = []
for o in traceback.format_exception(*exc_info):
o = ensure_text(o, get_filesystem_encoding())
if o.startswith(' File "'):
if _pluginpath_real and o.startswith(' File "'):
"""
If this split can't handle your file names,
you should seriously consider renaming your files.
@@ -114,7 +131,10 @@ class MyFormatter(logging.Formatter):
def _gen_rel_path(self, record):
if record.pathname:
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
try:
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
except Exception:
record.relpath = record.pathname
def get_filesystem_encoding():

View File

@@ -242,8 +242,10 @@ def play_file(play_info):
media_source_id = play_info.get("media_source_id", "")
subtitle_stream_index = play_info.get("subtitle_stream_index", None)
audio_stream_index = play_info.get("audio_stream_index", None)
force_track_selection = play_info.get("force_track_selection", False)
log.debug("playFile id({0}) resume({1}) force_transcode({2})".format(item_id, auto_resume, force_transcode))
log.debug("playFile id({0}) resume({1}) force_transcode({2}) force_track_selection({3})".format(
item_id, auto_resume, force_transcode, force_track_selection))
addon_path = settings.getAddonInfo('path')
force_auto_resume = settings.getSetting('forceAutoResume') == 'true'
@@ -461,7 +463,7 @@ def play_file(play_info):
if playback_type == "2": # if transcoding then prompt for audio and subtitle
playurl = audio_subs_pref(playurl, list_item, selected_media_source, item_id, audio_stream_index,
subtitle_stream_index)
subtitle_stream_index, force_track_selection)
log.debug("New playurl for transcoding: {0}".format(playurl))
elif playback_type == "1": # for direct stream add any streamable subtitles
@@ -778,12 +780,14 @@ def set_list_item_props(item_id, list_item, result, server, extra_props, title):
# Present the list of audio and subtitles to select from
# for external streamable subtitles add the URL to the Kodi item and let Kodi handle it
# else ask for the subtitles to be burnt in when transcoding
def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, subtitle_stream_index):
def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, subtitle_stream_index, force_track_selection=False):
dialog = xbmcgui.Dialog()
audio_streams_list = {}
audio_streams = []
audio_streams_data = [] # Store full stream data for preference matching
subtitle_streams_list = {}
subtitle_streams = ['No subtitles']
subtitle_streams_data = [] # Store full stream data for preference matching
downloadable_streams = []
select_audio_index = audio_stream_index
select_subs_index = subtitle_stream_index
@@ -792,6 +796,60 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s
default_sub = media_source.get('DefaultSubtitleStreamIndex', "")
source_id = media_source["Id"]
# Read user preferences
# Map select index to language code
lang_index_map = ['ger', 'eng', 'fra', 'spa', 'ita', 'jpn', 'rus', ''] # '' = other/custom
sub_lang_index_map = ['', 'ger', 'eng', 'fra', 'spa', 'ita', 'jpn', 'rus', ''] # First is 'none'
audio_lang_index = int(settings.getSetting("preferred_audio_language") or "0")
preferred_audio_lang = lang_index_map[audio_lang_index] if audio_lang_index < len(lang_index_map) else ''
auto_select_default_audio = settings.getSetting("auto_select_default_audio") == "true"
sub_lang_index = int(settings.getSetting("preferred_subtitle_language") or "1")
preferred_sub_lang = sub_lang_index_map[sub_lang_index] if sub_lang_index < len(sub_lang_index_map) else ''
prefer_forced = settings.getSetting("prefer_forced_subtitles") == "true"
only_forced = settings.getSetting("only_forced_subtitles") == "true"
# Subtitle codec preference: 0=PGS, 1=SRT, 2=No preference
codec_pref_index = int(settings.getSetting("subtitle_codec_preference") or "0")
prefer_pgs = codec_pref_index == 0
prefer_srt = codec_pref_index == 1
# codec_pref_index == 2 means no preference (both prefer_pgs and prefer_srt are False)
auto_no_subs = settings.getSetting("auto_no_subtitles_if_no_match") == "true"
# Language code mapping for better matching
language_map = {
'ger': ['ger', 'deu', 'de', 'german', 'deutsch'],
'eng': ['eng', 'en', 'english'],
'fra': ['fra', 'fr', 'fre', 'french', 'français'],
'spa': ['spa', 'es', 'spanish', 'español'],
'ita': ['ita', 'it', 'italian', 'italiano'],
'jpn': ['jpn', 'ja', 'japanese', '日本語'],
'rus': ['rus', 'ru', 'russian'],
}
# Helper function to check if language matches
def language_matches(stream_lang, preferred_lang):
stream_lang = stream_lang.lower()
preferred_lang = preferred_lang.lower()
# Direct match
if preferred_lang in stream_lang or stream_lang in preferred_lang:
return True
# Check against language map
for key, variations in language_map.items():
if preferred_lang in variations:
# Check if stream language matches any variation
for variant in variations:
if variant in stream_lang or stream_lang.startswith(variant[:2]):
return True
return False
media_streams = media_source['MediaStreams']
for stream in media_streams:
@@ -801,6 +859,7 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s
if 'Audio' in stream['Type']:
codec = stream.get('Codec', None)
channel_layout = stream.get('ChannelLayout', "")
title = stream.get('Title', '')
if not codec:
# Probably tvheadend and has no other info
@@ -813,30 +872,161 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s
# Track doesn't include language
track = "%s - %s %s" % (index, codec, channel_layout)
# Add title if available
if title:
track = "%s - %s" % (track, title)
audio_streams_list[track] = index
audio_streams.append(track)
audio_streams_data.append(stream)
elif 'Subtitle' in stream['Type']:
try:
# Track includes language
track = "%s - %s" % (index, stream['Language'])
except KeyError:
# Track doesn't include language
track = "%s - %s" % (index, stream['Codec'])
language = stream.get('Language', 'Unknown')
codec = stream.get('Codec', 'Unknown')
codec_names = {
'subrip': 'SRT',
'hdmv_pgs_subtitle': 'PGS',
'dvd_subtitle': 'VobSub',
'ass': 'ASS'
}
codec_display = codec_names.get(codec.lower(), codec.upper())
track = "%s - %s (%s)" % (index, language, codec_display)
default = stream['IsDefault']
forced = stream['IsForced']
hearing_impaired = stream.get('IsHearingImpaired', False)
downloadable = stream['IsTextSubtitleStream'] and stream['IsExternal'] and stream['SupportsExternalStream']
if default:
track = "%s - Default" % track
if forced:
track = "%s - Forced" % track
if hearing_impaired:
track = "%s - SDH" % track
if downloadable:
downloadable_streams.append(index)
subtitle_streams_list[track] = index
subtitle_streams.append(track)
subtitle_streams_data.append(stream)
# Auto-select audio track based on preferences
# Only auto-select if not already set by server/remote control AND not forcing manual selection
if not force_track_selection and select_audio_index is None and len(audio_streams_data) > 0 and (preferred_audio_lang or auto_select_default_audio):
auto_selected = None
best_score = -1
# Try to match preferred language with scoring
if preferred_audio_lang:
for stream in audio_streams_data:
score = 0
stream_lang = stream.get('Language', '')
# Match against common variations using language map
if language_matches(stream_lang, preferred_audio_lang):
score += 100 # Language match
# Bonus for default track
if stream.get('IsDefault', False):
score += 50
# Bonus based on channel count (more channels = better quality)
channels = stream.get('Channels', 2)
score += min(channels * 5, 40) # Max +40 for 8 channels
# Bonus for high-quality codecs
codec = stream.get('Codec', '').lower()
if 'truehd' in codec or 'dts-hd' in codec or 'dts-ma' in codec:
score += 30
elif 'dts' in codec:
score += 20
elif 'ac3' in codec or 'eac3' in codec:
score += 10
# Penalty for commentary tracks
title = stream.get('Title', '').lower()
display_title = stream.get('DisplayTitle', '').lower()
if 'commentary' in title or 'kommentar' in title or 'commentary' in display_title:
score -= 100 # Effectively exclude commentary
log.debug("Audio score for {0} ({1}): {2} (channels={3}, codec={4}, default={5})".format(
stream_lang, stream['Index'], score, channels, codec, stream.get('IsDefault', False)))
if score > best_score:
best_score = score
auto_selected = stream['Index']
# Fall back to default audio if enabled and no language match
if auto_selected is None and auto_select_default_audio:
auto_selected = default_audio
log.debug("Auto-selected default audio (index {0})".format(auto_selected))
if auto_selected is not None:
select_audio_index = auto_selected
if best_score > 0:
log.debug("Auto-selected audio (index {0}, score {1})".format(auto_selected, best_score))
# Auto-select subtitle track based on preferences
# Only auto-select if not already set by server/remote control AND not forcing manual selection
if force_track_selection:
log.debug("Forcing manual track selection (from context menu)")
elif select_subs_index is not None:
log.debug("Using subtitle index from server/remote: {0}".format(select_subs_index))
elif len(subtitle_streams_data) == 0:
log.debug("No subtitle streams available")
elif len(subtitle_streams_data) > 0 and preferred_sub_lang: # Only if user configured a language preference
auto_selected = None
best_score = -1
log.debug("Auto-selecting subtitle: preferred_lang={0}, prefer_forced={1}, only_forced={2}, prefer_pgs={3}, prefer_srt={4}".format(
preferred_sub_lang, prefer_forced, only_forced, prefer_pgs, prefer_srt))
for stream in subtitle_streams_data:
score = 0
stream_lang = stream.get('Language', '')
codec = stream.get('Codec', '').lower()
is_forced = stream.get('IsForced', False)
is_default = stream.get('IsDefault', False)
# Skip non-forced subtitles if only_forced is enabled
if only_forced and not is_forced:
log.debug("Skipping non-forced subtitle {0} ({1}) - only_forced is enabled".format(
stream_lang, stream['Index']))
continue
# Score based on language match
if preferred_sub_lang and language_matches(stream_lang, preferred_sub_lang):
score += 100
# Bonus for forced if preferred
if prefer_forced and is_forced:
score += 50
# Bonus for codec preference
if prefer_pgs and codec in ['hdmv_pgs_subtitle', 'pgs']:
score += 30
elif prefer_srt and codec in ['subrip', 'srt']:
score += 30
# Small bonus for default
if is_default:
score += 10
log.debug("Subtitle score for {0} ({1}): {2} (forced={3}, codec={4})".format(
stream_lang, stream['Index'], score, is_forced, codec))
if score > best_score:
best_score = score
auto_selected = stream['Index']
if auto_selected is not None and best_score >= 100: # Only auto-select if language matched
select_subs_index = auto_selected
log.debug("Auto-selected subtitle (index {0}, score {1})".format(auto_selected, best_score))
elif auto_no_subs and preferred_sub_lang: # No match found but user wants auto "no subs"
select_subs_index = -1 # Special value to indicate "no subtitles"
log.debug("No matching subtitle found - auto-selected 'No subtitles'")
# set audio index
if select_audio_index is not None:
@@ -854,8 +1044,12 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s
# set subtitle index
if select_subs_index is not None:
# Handle special "no subtitles" value
if select_subs_index == -1:
# User wants no subtitles - do nothing
pass
# Load subtitles in the listitem if downloadable
if select_subs_index in downloadable_streams:
elif select_subs_index in downloadable_streams:
subtitle_url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
subtitle_url = subtitle_url % (settings.getSetting('server_address'), item_id, source_id, select_subs_index)
log.debug("Streaming subtitles url: {0} {1}".format(select_subs_index, subtitle_url))
@@ -1283,13 +1477,22 @@ def get_play_url(media_source, play_session_id, channel_id=None):
audio_max_channels = settings.getSetting("audio_max_channels")
playback_video_force_8 = settings.getSetting("playback_video_force_8") == "true"
# Determine target video codec for transcoding
transcode_target_codec_setting = settings.getSetting("transcode_target_video_codec")
if transcode_target_codec_setting == "1":
transcode_video_codec = "hevc"
elif transcode_target_codec_setting == "2":
transcode_video_codec = "av1"
else:
transcode_video_codec = "h264"
transcode_params = {
"MediaSourceId": item_id,
"DeviceId": device_id,
"PlaySessionId": play_session_id,
"api_key": user_token,
"SegmentContainer": "ts",
"VideoCodec": "h264",
"VideoCodec": transcode_video_codec,
"VideoBitrate": bitrate,
"MaxWidth": playback_max_width,
"AudioCodec": audio_codec,
@@ -1534,7 +1737,12 @@ class PlaybackService(xbmc.Monitor):
def get_item_playback_info(item_id, force_transcode):
# Filter codecs that should NEVER be played directly (always force transcoding)
# These settings work independently from the target codec setting below
# Example: force_transcode_h264=true + target=hevc means: H.264 files will be transcoded to H.265
filtered_codecs = []
if settings.getSetting("force_transcode_h264") == "true":
filtered_codecs.append("h264")
if settings.getSetting("force_transcode_h265") == "true":
filtered_codecs.append("hevc")
filtered_codecs.append("h265")
@@ -1558,6 +1766,19 @@ def get_item_playback_info(item_id, force_transcode):
audio_bitrate = int(audio_playback_bitrate) * 1000
# Determine target video codec for transcoding
# Note: force_transcode_* settings filter codecs for DirectPlay independently
# This setting only affects what codec the server transcodes TO when transcoding is needed
transcode_target_codec_setting = settings.getSetting("transcode_target_video_codec")
log.debug("Transcode target codec setting value: '{0}'".format(transcode_target_codec_setting))
if transcode_target_codec_setting == "1":
transcode_video_codec = "hevc"
elif transcode_target_codec_setting == "2":
transcode_video_codec = "av1"
else:
transcode_video_codec = "h264"
log.debug("Transcode target video codec: {0}".format(transcode_video_codec))
profile = {
"Name": "Kodi",
"MaxStaticBitrate": bitrate,
@@ -1573,7 +1794,7 @@ def get_item_playback_info(item_id, force_transcode):
"Protocol": "hls",
"Type": "Video",
"AudioCodec": audio_codec,
"VideoCodec": "h264",
"VideoCodec": transcode_video_codec,
"MaxAudioChannels": audio_max_channels
},
{

File diff suppressed because it is too large Load Diff

View File

@@ -33,11 +33,11 @@ class WebSocketClient(threading.Thread):
self.__dict__ = self._shared_state
self.monitor = xbmc.Monitor()
self.retry_count = 0
self.device_id = get_device_id()
self._library_monitor = library_change_monitor
self.websocket_error = False
threading.Thread.__init__(self)
@@ -230,11 +230,16 @@ class WebSocketClient(threading.Thread):
xbmc.executebuiltin(builtin[command])
def on_open(self, ws):
# Wait to make sure previous keepalive cycle has ended
if self.websocket_error:
time.sleep(30)
self.websocket_error = False
log.debug("Connected")
self.retry_count = 0
self.post_capabilities()
self.send_keepalive(ws)
def on_error(self, ws, error):
self.websocket_error = True
log.debug("Error: {0}".format(error))
def run(self):
@@ -269,7 +274,7 @@ class WebSocketClient(threading.Thread):
while not self.monitor.abortRequested():
self._client.run_forever(ping_interval=5, reconnect=13, ping_timeout=2)
self._client.run_forever(reconnect=30)
if self._stop_websocket:
break
@@ -278,8 +283,6 @@ class WebSocketClient(threading.Thread):
# Abort was requested, exit
break
if self.retry_count < 12:
self.retry_count += 1
log.debug("Reconnecting WebSocket")
log.debug("WebSocketClient Stopped")
@@ -303,3 +306,22 @@ class WebSocketClient(threading.Thread):
)
api.post_capabilities()
def send_keepalive(self, ws):
# Stop the keepalive cycle if an error has been detected
if self.websocket_error:
return
keepalive_payload = json.dumps({"MessageType": "KeepAlive", "Data": 30})
# Send the keepalive, or register an error
try:
ws.send(keepalive_payload)
except:
self.websocket_error = True
return
# Schedule the next message
self.schedule_keepalive(ws)
def schedule_keepalive(self, ws):
# Schedule a keepalive message in 30 seconds
timer = threading.Timer(30, self.send_keepalive, kwargs={'ws': ws})
timer.start()

View File

@@ -0,0 +1,73 @@
@@ -46,7 +46,12 @@
result = json.loads(message)
message_type = result['MessageType']
- if message_type == 'Play':
+ if message_type == 'ForceKeepAlive':
+ timeout_seconds = result.get('Data', 60)
+ log.debug("Received ForceKeepAlive with timeout: {0}s".format(timeout_seconds))
+ self._send_keep_alive()
+
+ elif message_type == 'Play':
data = result['Data']
self._play(data)
@@ -237,6 +242,9 @@
def on_error(self, ws, error):
log.debug("Error: {0}".format(error))
+ def on_close(self, ws, close_status_code, close_msg):
+ log.debug("WebSocket closed. Code: {0}, Message: {1}".format(close_status_code, close_msg))
+
def run(self):
token = None
@@ -259,17 +267,23 @@
)
log.debug("websocket url: {0}".format(websocket_url))
- self._client = websocket.WebSocketApp(
- websocket_url,
- on_open=lambda ws: self.on_open(ws),
- on_message=lambda ws, message: self.on_message(ws, message),
- on_error=lambda ws, error: self.on_error(ws, error))
-
log.debug("Starting WebSocketClient")
while not self.monitor.abortRequested():
- self._client.run_forever(ping_interval=5, reconnect=13, ping_timeout=2)
+ # Create a new WebSocketApp for each connection attempt to avoid
+ # memory leaks from reusing a potentially dirty connection object
+ self._client = websocket.WebSocketApp(
+ websocket_url,
+ on_open=lambda ws: self.on_open(ws),
+ on_message=lambda ws, message: self.on_message(ws, message),
+ on_error=lambda ws, error: self.on_error(ws, error),
+ on_close=lambda ws, status, msg: self.on_close(ws, status, msg))
+
+ # Use ping_interval without ping_timeout to keep connection alive
+ # without forcing disconnection. The server's ForceKeepAlive/KeepAlive
+ # mechanism handles the actual keepalive logic.
+ self._client.run_forever(ping_interval=10)
if self._stop_websocket:
break
@@ -291,6 +305,17 @@
self._client.close()
log.debug("Stopping WebSocket (stop_client called)")
+ def _send_keep_alive(self):
+ """Send a KeepAlive message to the server to maintain the connection."""
+ try:
+ keep_alive_message = json.dumps({
+ 'MessageType': 'KeepAlive'
+ })
+ self._client.send(keep_alive_message)
+ log.debug("Sent KeepAlive message")
+ except Exception as error:
+ log.debug("Error sending KeepAlive: {0}".format(error))
+
def post_capabilities(self):
settings = xbmcaddon.Addon()

View File

@@ -30,6 +30,8 @@
<setting type="sep" />
<setting id="max_stream_bitrate" type="enum" label="30208" values="0.5 Mbps|1 Mbps|1.5 Mbps|2.0 Mbps|2.5 Mbps|3.0 Mbps|4.0 Mbps|5.0 Mbps|6.0 Mbps|7.0 Mbps|8.0 Mbps|9.0 Mbps|10.0 Mbps|12.0 Mbps|14.0 Mbps|16.0 Mbps|18.0 Mbps|20.0 Mbps|25.0 Mbps|30.0 Mbps|35.0 Mbps|40.0 Mbps|100.0 Mbps|1000.0 Mbps [default]|Maximum" visible="true" default="23" />
<setting id="allow_direct_file_play" type="bool" label="30433" default="false" visible="true" enable="true" />
<setting id="transcode_target_video_codec" type="select" label="30679" lvalues="30680|30681|30683" default="0" visible="true" />
<setting id="force_transcode_h264" type="bool" label="30682" default="false" visible="true" />
<setting id="force_transcode_h265" type="bool" label="30236" default="false" visible="true" enable="true" />
<setting id="force_transcode_mpeg2" type="bool" label="30239" default="false" visible="true" enable="true" />
<setting id="force_transcode_msmpeg4v3" type="bool" label="30240" default="false" visible="true" enable="true" />
@@ -45,6 +47,16 @@
<setting id="audio_codec" type="select" label="30419" values="ac3|aac" default="ac3"/>
<setting id="audio_playback_bitrate" type="select" label="30418" values="128|160|192|256|320|384|448|640" default="256" visible="true"/>
<setting id="audio_max_channels" type="slider" label="30420" default="8" range="2,1,8" option="int" visible="true"/>
<setting label="30685" type="lsep"/>
<setting type="sep" />
<setting id="preferred_audio_language" type="select" label="30686" lvalues="30691|30692|30693|30694|30695|30696|30697|30698" default="0" visible="true"/>
<setting id="auto_select_default_audio" type="bool" label="30687" default="true" visible="true"/>
<setting id="preferred_subtitle_language" type="select" label="30688" lvalues="30699|30691|30692|30693|30694|30695|30696|30697|30698" default="1" visible="true"/>
<setting id="prefer_forced_subtitles" type="bool" label="30689" default="true" visible="true"/>
<setting id="only_forced_subtitles" type="bool" label="30702" default="true" visible="true"/>
<setting id="subtitle_codec_preference" type="select" label="30703" lvalues="30704|30705|30706" default="0" visible="true"/>
<setting id="auto_no_subtitles_if_no_match" type="bool" label="30700" default="true" visible="true"/>
<setting id="max_play_queue" type="slider" label="30447" default="200" range="20, 10, 1000" option="int" visible="true"/>
</category>
@@ -157,7 +169,8 @@
<setting id="profile_count" type="slider" label="30010" default="0" range="0,1,20" option="int" visible="true" />
<setting id="log_debug" type="bool" label="30027" default="false" visible="true" enable="true" />
<setting id="log_timing" type="bool" label="30015" default="false" visible="true" enable="true" />
<setting id="use_cache" type="bool" label="30345" default="true" visible="true" enable="true" />
<setting id="disable_disk_cache" type="bool" label="30684" default="true" visible="true" enable="true" />
<setting id="use_cache" type="bool" label="30345" default="false" visible="true" enable="true" />
<setting id="use_cached_widget_data" type="bool" label="30441" default="false" visible="true" enable="true" />
<setting id="showLoadProgress" type="bool" label="30120" default="false" visible="true" enable="true" />
<setting id="suppressErrors" type="bool" label="30315" default="false" visible="true" enable="true" />

View File

@@ -22,11 +22,14 @@ from resources.lib.image_server import HttpImageServerThread
from resources.lib.playnext import PlayNextService
from resources.lib.intro_skipper import IntroSkipperService
settings = xbmcaddon.Addon()
log_timing_data = settings.getSetting('log_timing') == "true"
if log_timing_data:
set_timing_enabled(True)
try:
settings = xbmcaddon.Addon()
log_timing_data = settings.getSetting('log_timing') == "true"
if log_timing_data:
set_timing_enabled(True)
except Exception:
# During installation/update, addon might not be fully registered yet
pass
# clear user and token when logging in
home_window = HomeWindow()