jmoona-cli 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.4
2
+ Name: jmoona-cli
3
+ Version: 1.0.0
4
+ Summary: Films & séries du monde en streaming depuis le terminal
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/yourusername/jmoona-cli
7
+ Keywords: streaming,films,séries,mpv,cli,terminal
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: requests>=2.28.0
11
+ Requires-Dist: selenium>=4.9.0
12
+ Requires-Dist: undetected-chromedriver>=3.5.0
13
+ Requires-Dist: pyvirtualdisplay>=3.0; sys_platform == "linux"
14
+ Requires-Dist: curl-cffi>=0.6.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+ Requires-Dist: black; extra == "dev"
18
+ Requires-Dist: ruff; extra == "dev"
19
+
20
+ # 🎬 jmoona-cli
21
+
22
+ ```
23
+ _ _ _
24
+ (_) | (_)
25
+ _ _ __ ___ ___ ___ _ __ __ _ ___| |_
26
+ | | '_ ` _ \ / _ \ / _ \| '_ \ / _` |/ __| | |
27
+ | | | | | | | (_) | (_) | | | | (_| | (__| | |
28
+ | |_| |_| |_|\___/ \___/|_| |_|\__,_|\___|_|_|
29
+ ```
30
+
31
+ > **L'émulateur ultime de films et séries** — streaming depuis votre terminal
32
+
33
+ ---
34
+
35
+ ## ✨ Fonctionnalités
36
+
37
+ - 🔍 Recherche de films & séries via TMDB
38
+ - 🎬 Streaming en un clic via `mpv`
39
+ - 🔤 Sous-titres français automatiques (OpenSubtitles)
40
+ - 📥 Téléchargement avec `yt-dlp`
41
+ - 🕹️ Reprise de lecture automatique
42
+ - 💾 Historique de visionnage
43
+ - 🌐 Multi-sources (Cloudnestra, vidsrc, smashystream, ...)
44
+ - 🖥️ Compatible Linux · macOS · Windows · Docker
45
+
46
+ ---
47
+
48
+ ## 📦 Installation rapide
49
+
50
+ ### 🐧 Linux / 🍎 macOS
51
+
52
+ ```bash
53
+ bash <(curl -fsSL https://raw.githubusercontent.com/riter675/jmoona-cli/main/install.sh)
54
+ ```
55
+
56
+ ### 🪟 Windows (PowerShell en administrateur)
57
+
58
+ ```powershell
59
+ irm https://raw.githubusercontent.com/riter675/jmoona-cli/main/install.ps1 | iex
60
+ ```
61
+
62
+ ### 🐳 Docker (n'importe quel OS)
63
+
64
+ ```bash
65
+ docker build -t jmoona .
66
+ # Avec affichage vidéo sur l'écran hôte (Linux) :
67
+ docker run -it --rm \
68
+ -e DISPLAY=$DISPLAY \
69
+ -v /tmp/.X11-unix:/tmp/.X11-unix \
70
+ -v "$HOME/Downloads/jmoona:/root/Downloads/jmoona" \
71
+ jmoona
72
+ ```
73
+
74
+ ### Installation manuelle (pip)
75
+
76
+ ```bash
77
+ git clone https://github.com/riter675/jmoona-cli.git
78
+ cd jmoona-cli
79
+ pip install -e .
80
+ jmoona
81
+ ```
82
+
83
+ ---
84
+
85
+ ## ⚠️ Prérequis
86
+
87
+ | Outil | Rôle | Requis |
88
+ |-------|------|--------|
89
+ | Python 3.10+ | Moteur | ✅ Obligatoire |
90
+ | `mpv` | Lecteur vidéo | ✅ Obligatoire |
91
+ | `yt-dlp` | Extracteur de streams | ✅ Obligatoire |
92
+ | `fzf` | Interface menus améliorée | ⚡ Recommandé |
93
+ | `ffmpeg` / `ffprobe` | Analyse des pistes | ⚡ Recommandé |
94
+ | Chromium + chromedriver | Extraction Selenium | 🔧 Optionnel |
95
+
96
+ ### Installation des prérequis
97
+
98
+ **Ubuntu/Debian:**
99
+ ```bash
100
+ sudo apt install mpv yt-dlp fzf ffmpeg python3 python3-pip
101
+ pip install jmoona-cli
102
+ ```
103
+
104
+ **Fedora:**
105
+ ```bash
106
+ sudo dnf install mpv yt-dlp fzf ffmpeg python3 python3-pip
107
+ ```
108
+
109
+ **Arch Linux:**
110
+ ```bash
111
+ sudo pacman -S mpv yt-dlp fzf ffmpeg python python-pip
112
+ ```
113
+
114
+ **macOS (Homebrew):**
115
+ ```bash
116
+ brew install mpv yt-dlp fzf ffmpeg python
117
+ pip3 install jmoona-cli
118
+ ```
119
+
120
+ **Windows (winget + pip):**
121
+ ```powershell
122
+ winget install mpv.mpv yt-dlp.yt-dlp Git.Git Python.Python.3.12
123
+ pip install jmoona-cli
124
+ ```
125
+
126
+ ---
127
+
128
+ ## 🚀 Utilisation
129
+
130
+ ```bash
131
+ jmoona # Lancer l'interface interactive
132
+ jmoona "Inception" # Recherche directe
133
+ ```
134
+
135
+ ### Navigation
136
+ - `↑↓` ou numéro — choisir dans les menus
137
+ - `Enter` — valider
138
+ - `q` ou `Ctrl+C` — quitter
139
+ - Dans mpv : `f` = plein écran, `←→` = avance/recul, `q` = quitter
140
+
141
+ ### Modes de lecture
142
+
143
+ | Mode | Description |
144
+ |------|-------------|
145
+ | 🎵 **Auto** | FR si disponible, anglais sinon (recommandé) |
146
+ | 🔤 **VOSTFR** | Audio original + sous-titres français (idéal animés) |
147
+ | 🇬🇧 **VA** | Version originale sans sous-titres |
148
+
149
+ ---
150
+
151
+ ## ⚙️ Configuration
152
+
153
+ Fichier de config par OS :
154
+ - **Linux** : `~/.config/jmoona/config.json`
155
+ - **macOS** : `~/Library/Application Support/jmoona/config.json`
156
+ - **Windows** : `%APPDATA%\jmoona\config.json`
157
+
158
+ Options disponibles :
159
+
160
+ ```json
161
+ {
162
+ "player": "mpv",
163
+ "player_args": "--fs",
164
+ "quality": "best",
165
+ "provider": "auto",
166
+ "use_fzf": true,
167
+ "resume": true,
168
+ "download_dir": "~/Downloads/jmoona"
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 🔧 Dépendances Python
175
+
176
+ ```
177
+ requests>=2.28.0
178
+ selenium>=4.9.0
179
+ undetected-chromedriver>=3.5.0
180
+ pyvirtualdisplay>=3.0 # Linux uniquement
181
+ curl-cffi>=0.6.0
182
+ ```
183
+
184
+ ---
185
+
186
+ ## 📁 Structure du projet
187
+
188
+ ```
189
+ jmoona-cli/
190
+ ├── jmoona/
191
+ │ ├── app.py # Logique principale
192
+ │ ├── cli.py # Point d'entrée CLI
193
+ │ ├── extractor.py # Extraction des streams
194
+ │ ├── player.py # Lecture via mpv
195
+ │ ├── downloader.py # Téléchargement via yt-dlp
196
+ │ ├── subtitles.py # Sous-titres via OpenSubtitles
197
+ │ ├── tmdb.py # API TMDB
198
+ │ ├── ui.py # Interface terminal
199
+ │ ├── art.py # ASCII art
200
+ │ ├── language.py # Détection pistes audio
201
+ │ ├── storage.py # Historique & reprise
202
+ │ ├── config.py # Configuration cross-platform
203
+ │ └── providers.py # Liste des providers
204
+ ├── pyproject.toml
205
+ ├── install.sh # Installateur Linux/macOS
206
+ ├── install.ps1 # Installateur Windows
207
+ ├── Dockerfile
208
+ └── README.md
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 🐛 Problèmes courants
214
+
215
+ **`mpv: command not found`** → Installez mpv (voir prérequis ci-dessus)
216
+
217
+ **Stream introuvable** → Essayez un autre provider : `jmoona` → paramètres → providers
218
+
219
+ **Chromium manquant** → Normal, Selenium est optionnel. Les streams HTTP fonctionnent sans.
220
+
221
+ **Windows : couleurs absentes** → Activez les couleurs ANSI : `Set-ItemProperty HKCU:\Console VirtualTerminalLevel 1`
222
+
223
+ ---
224
+
225
+ ## 📄 Licence
226
+
227
+ MIT — fait avec ❤️ par jmoona
@@ -0,0 +1,208 @@
1
+ # 🎬 jmoona-cli
2
+
3
+ ```
4
+ _ _ _
5
+ (_) | (_)
6
+ _ _ __ ___ ___ ___ _ __ __ _ ___| |_
7
+ | | '_ ` _ \ / _ \ / _ \| '_ \ / _` |/ __| | |
8
+ | | | | | | | (_) | (_) | | | | (_| | (__| | |
9
+ | |_| |_| |_|\___/ \___/|_| |_|\__,_|\___|_|_|
10
+ ```
11
+
12
+ > **L'émulateur ultime de films et séries** — streaming depuis votre terminal
13
+
14
+ ---
15
+
16
+ ## ✨ Fonctionnalités
17
+
18
+ - 🔍 Recherche de films & séries via TMDB
19
+ - 🎬 Streaming en un clic via `mpv`
20
+ - 🔤 Sous-titres français automatiques (OpenSubtitles)
21
+ - 📥 Téléchargement avec `yt-dlp`
22
+ - 🕹️ Reprise de lecture automatique
23
+ - 💾 Historique de visionnage
24
+ - 🌐 Multi-sources (Cloudnestra, vidsrc, smashystream, ...)
25
+ - 🖥️ Compatible Linux · macOS · Windows · Docker
26
+
27
+ ---
28
+
29
+ ## 📦 Installation rapide
30
+
31
+ ### 🐧 Linux / 🍎 macOS
32
+
33
+ ```bash
34
+ bash <(curl -fsSL https://raw.githubusercontent.com/riter675/jmoona-cli/main/install.sh)
35
+ ```
36
+
37
+ ### 🪟 Windows (PowerShell en administrateur)
38
+
39
+ ```powershell
40
+ irm https://raw.githubusercontent.com/riter675/jmoona-cli/main/install.ps1 | iex
41
+ ```
42
+
43
+ ### 🐳 Docker (n'importe quel OS)
44
+
45
+ ```bash
46
+ docker build -t jmoona .
47
+ # Avec affichage vidéo sur l'écran hôte (Linux) :
48
+ docker run -it --rm \
49
+ -e DISPLAY=$DISPLAY \
50
+ -v /tmp/.X11-unix:/tmp/.X11-unix \
51
+ -v "$HOME/Downloads/jmoona:/root/Downloads/jmoona" \
52
+ jmoona
53
+ ```
54
+
55
+ ### Installation manuelle (pip)
56
+
57
+ ```bash
58
+ git clone https://github.com/riter675/jmoona-cli.git
59
+ cd jmoona-cli
60
+ pip install -e .
61
+ jmoona
62
+ ```
63
+
64
+ ---
65
+
66
+ ## ⚠️ Prérequis
67
+
68
+ | Outil | Rôle | Requis |
69
+ |-------|------|--------|
70
+ | Python 3.10+ | Moteur | ✅ Obligatoire |
71
+ | `mpv` | Lecteur vidéo | ✅ Obligatoire |
72
+ | `yt-dlp` | Extracteur de streams | ✅ Obligatoire |
73
+ | `fzf` | Interface menus améliorée | ⚡ Recommandé |
74
+ | `ffmpeg` / `ffprobe` | Analyse des pistes | ⚡ Recommandé |
75
+ | Chromium + chromedriver | Extraction Selenium | 🔧 Optionnel |
76
+
77
+ ### Installation des prérequis
78
+
79
+ **Ubuntu/Debian:**
80
+ ```bash
81
+ sudo apt install mpv yt-dlp fzf ffmpeg python3 python3-pip
82
+ pip install jmoona-cli
83
+ ```
84
+
85
+ **Fedora:**
86
+ ```bash
87
+ sudo dnf install mpv yt-dlp fzf ffmpeg python3 python3-pip
88
+ ```
89
+
90
+ **Arch Linux:**
91
+ ```bash
92
+ sudo pacman -S mpv yt-dlp fzf ffmpeg python python-pip
93
+ ```
94
+
95
+ **macOS (Homebrew):**
96
+ ```bash
97
+ brew install mpv yt-dlp fzf ffmpeg python
98
+ pip3 install jmoona-cli
99
+ ```
100
+
101
+ **Windows (winget + pip):**
102
+ ```powershell
103
+ winget install mpv.mpv yt-dlp.yt-dlp Git.Git Python.Python.3.12
104
+ pip install jmoona-cli
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 🚀 Utilisation
110
+
111
+ ```bash
112
+ jmoona # Lancer l'interface interactive
113
+ jmoona "Inception" # Recherche directe
114
+ ```
115
+
116
+ ### Navigation
117
+ - `↑↓` ou numéro — choisir dans les menus
118
+ - `Enter` — valider
119
+ - `q` ou `Ctrl+C` — quitter
120
+ - Dans mpv : `f` = plein écran, `←→` = avance/recul, `q` = quitter
121
+
122
+ ### Modes de lecture
123
+
124
+ | Mode | Description |
125
+ |------|-------------|
126
+ | 🎵 **Auto** | FR si disponible, anglais sinon (recommandé) |
127
+ | 🔤 **VOSTFR** | Audio original + sous-titres français (idéal animés) |
128
+ | 🇬🇧 **VA** | Version originale sans sous-titres |
129
+
130
+ ---
131
+
132
+ ## ⚙️ Configuration
133
+
134
+ Fichier de config par OS :
135
+ - **Linux** : `~/.config/jmoona/config.json`
136
+ - **macOS** : `~/Library/Application Support/jmoona/config.json`
137
+ - **Windows** : `%APPDATA%\jmoona\config.json`
138
+
139
+ Options disponibles :
140
+
141
+ ```json
142
+ {
143
+ "player": "mpv",
144
+ "player_args": "--fs",
145
+ "quality": "best",
146
+ "provider": "auto",
147
+ "use_fzf": true,
148
+ "resume": true,
149
+ "download_dir": "~/Downloads/jmoona"
150
+ }
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 🔧 Dépendances Python
156
+
157
+ ```
158
+ requests>=2.28.0
159
+ selenium>=4.9.0
160
+ undetected-chromedriver>=3.5.0
161
+ pyvirtualdisplay>=3.0 # Linux uniquement
162
+ curl-cffi>=0.6.0
163
+ ```
164
+
165
+ ---
166
+
167
+ ## 📁 Structure du projet
168
+
169
+ ```
170
+ jmoona-cli/
171
+ ├── jmoona/
172
+ │ ├── app.py # Logique principale
173
+ │ ├── cli.py # Point d'entrée CLI
174
+ │ ├── extractor.py # Extraction des streams
175
+ │ ├── player.py # Lecture via mpv
176
+ │ ├── downloader.py # Téléchargement via yt-dlp
177
+ │ ├── subtitles.py # Sous-titres via OpenSubtitles
178
+ │ ├── tmdb.py # API TMDB
179
+ │ ├── ui.py # Interface terminal
180
+ │ ├── art.py # ASCII art
181
+ │ ├── language.py # Détection pistes audio
182
+ │ ├── storage.py # Historique & reprise
183
+ │ ├── config.py # Configuration cross-platform
184
+ │ └── providers.py # Liste des providers
185
+ ├── pyproject.toml
186
+ ├── install.sh # Installateur Linux/macOS
187
+ ├── install.ps1 # Installateur Windows
188
+ ├── Dockerfile
189
+ └── README.md
190
+ ```
191
+
192
+ ---
193
+
194
+ ## 🐛 Problèmes courants
195
+
196
+ **`mpv: command not found`** → Installez mpv (voir prérequis ci-dessus)
197
+
198
+ **Stream introuvable** → Essayez un autre provider : `jmoona` → paramètres → providers
199
+
200
+ **Chromium manquant** → Normal, Selenium est optionnel. Les streams HTTP fonctionnent sans.
201
+
202
+ **Windows : couleurs absentes** → Activez les couleurs ANSI : `Set-ItemProperty HKCU:\Console VirtualTerminalLevel 1`
203
+
204
+ ---
205
+
206
+ ## 📄 Licence
207
+
208
+ MIT — fait avec ❤️ par jmoona
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,225 @@
1
+ import sys
2
+ import os
3
+ from .ui import C, fzf_or_numbered, _strip_ansi, success, warn, error, clear_line, spinner
4
+ from .tmdb import tmdb_client
5
+ from .storage import load_config, add_history, get_history, get_bookmarks, add_bookmark, get_resume
6
+ from .extractor import extract
7
+ from .player import play
8
+ from .language import detect_tracks, get_lang_label
9
+ from .downloader import download
10
+ from .art import get_random_art
11
+ from .subtitles import fetch_subtitle
12
+
13
+ def format_item(item):
14
+ title = item.get("title") or item.get("name") or "Inconnu"
15
+ date = (item.get("release_date") or item.get("first_air_date") or "")[:4]
16
+ date_str = f"({date})" if date else ""
17
+ vt = item.get("vote_count", 0)
18
+ if vt > 1000: vt_str = f"{vt/1000:.1f}k"
19
+ else: vt_str = str(vt)
20
+ icon = "🎬" if item.get("media_type", "movie") == "movie" else "📺"
21
+ return f"{icon} {C.BOLD}{title}{C.RESET} {C.DIM}{date_str}{C.RESET} {C.YELLOW}★{item.get('vote_average', 0):.1f}{C.RESET} {C.DIM}{vt_str} votes{C.RESET}"
22
+
23
+ def print_details(item):
24
+ title = item.get("title") or item.get("name") or "Inconnu"
25
+ date = (item.get("release_date") or item.get("first_air_date") or "")[:4]
26
+ mtype = "🎬 Film" if item.get("media_type", "movie") == "movie" else "📺 Série"
27
+ overview = item.get("overview", "Aucune description.")
28
+ if len(overview) > 150:
29
+ overview = overview[:147] + "..."
30
+
31
+ print(f"\n{C.CYAN}════════════════════════════════════════════════════════════{C.RESET}")
32
+ print(f" {C.BOLD}{title}{C.RESET} ({date}) {mtype}")
33
+ print(f" {C.YELLOW}★{item.get('vote_average', 0):.1f}/10{C.RESET} | {item.get('vote_count', 0)} votes")
34
+ if "original_language" in item:
35
+ print(f" Langue originale: {get_lang_label(item['original_language'])}")
36
+ print(f"\n {C.DIM}{overview}{C.RESET}")
37
+ print(f"{C.CYAN}════════════════════════════════════════════════════════════{C.RESET}")
38
+
39
+ def select_media(results, config, prompt="Résultats"):
40
+ if not results:
41
+ warn("Aucun résultat.")
42
+ return None
43
+ return fzf_or_numbered(results, prompt, format_item, use_fzf=config.get("use_fzf", True))
44
+
45
+ def main_flow(query=None, args=None):
46
+ config = load_config()
47
+ if args and getattr(args, 'no_fzf', False):
48
+ config["use_fzf"] = False
49
+
50
+ if query:
51
+ spinner(f"Recherche de '{query}'...", art=get_random_art())
52
+ results = tmdb_client.search(query, media_type=getattr(args, 'type', 'multi') or 'multi')
53
+ clear_line()
54
+ item = select_media(results, config, "Choisir un titre")
55
+ if item: handle_item(item, args, config)
56
+ return
57
+
58
+ menu = [
59
+ ("🎬 Chercher un film", lambda: handle_search("movie", args, config)),
60
+ ("📺 Chercher une série", lambda: handle_search("tv", args, config)),
61
+ ("🔥 Tendances", lambda: handle_list(tmdb_client.trending(), "Tendances", args, config)),
62
+ ("⭐ Films populaires", lambda: handle_list(tmdb_client.popular("movie"), "Films Populaires", args, config)),
63
+ ("📡 Séries populaires", lambda: handle_list(tmdb_client.popular("tv"), "Séries Populaires", args, config)),
64
+ ("🏆 Mieux notés", lambda: handle_list(tmdb_client.top_rated("movie"), "Mieux Notés", args, config)),
65
+ ("🕒 Historique", lambda: handle_history(args, config)),
66
+ ("🔖 Favoris", lambda: handle_bookmarks(args, config)),
67
+ ("👋 Quitter", lambda: sys.exit(0))
68
+ ]
69
+
70
+ ASCII_LOGO = rf"""{C.MAGENTA}
71
+ _ _ _
72
+ (_) | (_)
73
+ _ _ __ ___ ___ ___ _ __ __ _ ___| |_
74
+ | | '_ ` _ \ / _ \ / _ \| '_ \ / _` |/ __| | |
75
+ | | | | | | | (_) | (_) | | | | (_| | (__| | |
76
+ | |_| |_| |_|\___/ \___/|_| |_|\__,_|\___|_|_|
77
+ _/ |
78
+ |__/ {C.RESET}{C.DIM}v1.0.0 - L'émulateur ultime de films et séries{C.RESET}
79
+ """
80
+ if not query:
81
+ print("\033c", end="") # Clear screen
82
+ print(ASCII_LOGO)
83
+ print(f" {C.BOLD}{C.CYAN}Bienvenue sur votre émulateur de films et séries by jmoona.{C.RESET}\n")
84
+ print(get_random_art())
85
+ try:
86
+ input(f"\n {C.DIM}Appuyez sur Entrée pour continuer...{C.RESET}")
87
+ except (KeyboardInterrupt, EOFError):
88
+ sys.exit(0)
89
+ print("\033c", end="") # Clear screen again before menu
90
+ print(ASCII_LOGO)
91
+ print(f" {C.BOLD}{C.CYAN}Menu Principal{C.RESET}\n")
92
+
93
+ choice = fzf_or_numbered(menu, "Menu principal", lambda x: x[0], use_fzf=config.get("use_fzf", True))
94
+ if choice:
95
+ choice[1]()
96
+
97
+ def handle_search(mtype, args, config):
98
+ q = input(f"\nRecherche ({'Film' if mtype == 'movie' else 'Série'}) : ").strip()
99
+ if q:
100
+ spinner("Recherche...", art=get_random_art())
101
+ results = tmdb_client.search(q, media_type=mtype)
102
+ clear_line()
103
+ item = select_media(results, config, "Résultats")
104
+ if item:
105
+ item["media_type"] = mtype
106
+ handle_item(item, args, config)
107
+
108
+ def handle_list(items, title, args, config):
109
+ item = select_media(items, config, title)
110
+ if item:
111
+ if "media_type" not in item:
112
+ item["media_type"] = "tv" if "first_air_date" in item else "movie"
113
+ handle_item(item, args, config)
114
+
115
+ def handle_history(args, config):
116
+ history = get_history()
117
+ if not history:
118
+ warn("Historique vide.")
119
+ return
120
+ item = select_media(history, config, "Historique")
121
+ if item: handle_item(item, args, config)
122
+
123
+ def handle_bookmarks(args, config):
124
+ bookmarks = get_bookmarks()
125
+ if not bookmarks:
126
+ warn("Favoris vides.")
127
+ return
128
+ item = select_media(bookmarks, config, "Favoris")
129
+ if item: handle_item(item, args, config)
130
+
131
+ def handle_item(item, args, config):
132
+ if "media_type" not in item:
133
+ item["media_type"] = "tv" if "first_air_date" in item else "movie"
134
+
135
+ print_details(item)
136
+ opts = ["▶ Regarder", "⬇ Télécharger", "🔖 Ajouter aux favoris", "🔙 Retour"]
137
+ choice = fzf_or_numbered(opts, "Action", lambda x: x, use_fzf=config.get("use_fzf", True))
138
+
139
+ if not choice or "Retour" in choice:
140
+ return
141
+
142
+ if "Favoris" in choice:
143
+ add_bookmark(item)
144
+ success("Ajouté aux favoris.")
145
+ return
146
+
147
+ season, episode = 1, 1
148
+ if item["media_type"] == "tv":
149
+ if args and getattr(args, 'season', None) and getattr(args, 'episode', None):
150
+ season, episode = args.season, args.episode
151
+ else:
152
+ try:
153
+ s_str = input("Saison (défaut=1) : ").strip()
154
+ if s_str: season = int(s_str)
155
+ e_str = input("Épisode (défaut=1) : ").strip()
156
+ if e_str: episode = int(e_str)
157
+ except ValueError:
158
+ pass
159
+
160
+ provider = args.provider if args and getattr(args, 'provider', None) else config.get("provider", "auto")
161
+ quality = args.quality if args and getattr(args, 'quality', None) else config.get("quality", "best")
162
+
163
+ # Mode de lecture (simple)
164
+ is_anime = item.get("original_language") in ("ja", "ko", "zh")
165
+ lang_menu = [
166
+ ("🎵 Auto — FR si disponible, sinon VO (recommandé)", "auto"),
167
+ ("🔤 VOSTFR — VO + sous-titres français", "vostfr"),
168
+ ("🇬🇧 VA — Version originale uniquement", "va"),
169
+ ]
170
+ lang_choice = fzf_or_numbered(lang_menu, "Mode de lecture", lambda x: x[0], use_fzf=config.get("use_fzf", True))
171
+ lang_mode = lang_choice[1] if lang_choice else ("vostfr" if is_anime else "auto")
172
+
173
+ stream_url, _ = extract(item["id"], item["media_type"], season, episode, quality=quality, lang="en", provider=provider)
174
+
175
+ if not stream_url:
176
+ error("Impossible de trouver un flux vidéo.")
177
+ return
178
+
179
+ # Audio language preference for mpv (no slow track scan needed)
180
+ audio_lang_mpv = {
181
+ "auto": "fr,en", # mpv picks FR if available, falls back to EN
182
+ "va": "en",
183
+ "vostfr": "en", # keep original audio, subs handle French
184
+ }.get(lang_mode, "en")
185
+
186
+ # Subtitles — VOSTFR only
187
+ sub_file = None
188
+ if lang_mode == "vostfr":
189
+ spinner("Recherche de sous-titres français ...", art=get_random_art())
190
+ sub_file = fetch_subtitle(item["id"], item["media_type"], lang="fr", season=season, episode=episode)
191
+ clear_line()
192
+ if sub_file:
193
+ success(f"✓ VOSTFR — {os.path.basename(sub_file)}")
194
+ else:
195
+ warn("Aucun sous-titre FR disponible sur OpenSubtitles — lecture en VO.")
196
+
197
+
198
+ item["episode"] = episode
199
+ add_history(item)
200
+
201
+ if "Télécharger" in choice:
202
+ out_dir = os.path.expanduser(config.get("download_dir", "~/Downloads/jmoona"))
203
+ title = item.get("title") or item.get("name")
204
+ if item["media_type"] == "tv":
205
+ title += f" S{season:02d}E{episode:02d}"
206
+
207
+ download(stream_url, title, out_dir, quality=quality,
208
+ audio_lang=audio_lang_mpv or "en",
209
+ sub_path=sub_file)
210
+ else:
211
+ rkey = f"{item['media_type']}_{item['id']}"
212
+ if item["media_type"] == "tv":
213
+ rkey += f"_s{season}e{episode}"
214
+
215
+ resume_pos = get_resume(rkey)
216
+
217
+ play(
218
+ stream_url,
219
+ title=(item.get("title") or item.get("name")),
220
+ player=config.get("player", "mpv"),
221
+ player_args=config.get("player_args", "--fs"),
222
+ audio_lang=audio_lang_mpv,
223
+ sub_path=sub_file,
224
+ resume_pos=resume_pos
225
+ )