Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import gradio as gr
|
2 |
-
import requests, zipfile, os, shutil, json
|
3 |
|
4 |
# -----------------------
|
5 |
# Free API keys (TMDb e OMDb) ottenute automaticamente
|
@@ -17,8 +17,10 @@ free_keys = get_free_keys()
|
|
17 |
TMDB_API_KEY = free_keys["tmdb_key"]
|
18 |
|
19 |
# -----------------------
|
20 |
-
# FILE DI CONFIGURAZIONE
|
21 |
SETTINGS_FILE = "settings.json"
|
|
|
|
|
22 |
DEFAULT_SETTINGS = {
|
23 |
"videolibrarypath": "path/to/videolibrary",
|
24 |
"folder_tvshows": "Serie TV",
|
@@ -61,6 +63,46 @@ def update_settings(videolibrarypath, folder_tvshows, folder_movies, videolibrar
|
|
61 |
}
|
62 |
return save_settings(new_settings)
|
63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
# -----------------------
|
65 |
# FUNZIONALITÀ DI AGGIORNAMENTO DELL'APPLICAZIONE
|
66 |
def get_branches():
|
@@ -85,8 +127,7 @@ def get_branches():
|
|
85 |
def update_from_zip(branch: str) -> str:
|
86 |
"""
|
87 |
Scarica il file zip del branch selezionato da GitHub,
|
88 |
-
lo estrae in una cartella target ("s4me_app") e restituisce un log
|
89 |
-
Questa funzione è completamente stand-alone e non utilizza dipendenze da Kodi.
|
90 |
"""
|
91 |
log = []
|
92 |
try:
|
@@ -101,11 +142,9 @@ def update_from_zip(branch: str) -> str:
|
|
101 |
f.write(chunk)
|
102 |
log.append("Download completato.")
|
103 |
|
104 |
-
# Definizione delle directory: estraiamo in una cartella temporanea
|
105 |
extract_base = f"s4me_app_{branch}"
|
106 |
target_dir = "s4me_app"
|
107 |
|
108 |
-
# Rimuovo la cartella target se esiste già
|
109 |
if os.path.isdir(target_dir):
|
110 |
shutil.rmtree(target_dir)
|
111 |
log.append(f"Vecchia cartella '{target_dir}' rimossa.")
|
@@ -115,7 +154,6 @@ def update_from_zip(branch: str) -> str:
|
|
115 |
zip_ref.extractall(extract_base)
|
116 |
log.append("Estrazione completata.")
|
117 |
|
118 |
-
# GitHub estrae in una cartella denominata 'addon-<branch>'
|
119 |
extracted_folder = os.path.join(extract_base, f"addon-{branch}")
|
120 |
if os.path.isdir(extracted_folder):
|
121 |
os.rename(extracted_folder, target_dir)
|
@@ -123,7 +161,6 @@ def update_from_zip(branch: str) -> str:
|
|
123 |
else:
|
124 |
log.append("Errore: cartella estratta non trovata.")
|
125 |
|
126 |
-
# Pulizia: rimozione del file zip e della cartella temporanea
|
127 |
os.remove(zip_filename)
|
128 |
shutil.rmtree(extract_base)
|
129 |
log.append("Pulizia completata. Aggiornamento eseguito con successo.")
|
@@ -136,39 +173,116 @@ def perform_update(selected_branch: str) -> str:
|
|
136 |
return update_from_zip(selected_branch)
|
137 |
|
138 |
# -----------------------
|
139 |
-
# FUNZIONALITÀ DI RICERCA FILM
|
140 |
-
def
|
141 |
"""
|
142 |
-
Cerca
|
143 |
-
|
144 |
"""
|
145 |
if not movie_title:
|
146 |
-
return
|
147 |
url = f"https://api.themoviedb.org/3/search/movie?api_key={TMDB_API_KEY}&query={movie_title}"
|
148 |
try:
|
149 |
response = requests.get(url)
|
150 |
response.raise_for_status()
|
151 |
data = response.json()
|
152 |
results = data.get("results", [])
|
153 |
-
|
154 |
-
return "Nessun risultato trovato."
|
155 |
-
output = []
|
156 |
for movie in results:
|
157 |
title = movie.get("title", "Sconosciuto")
|
158 |
release_date = movie.get("release_date", "Data sconosciuta")
|
159 |
-
|
160 |
-
|
161 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
except Exception as e:
|
163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
|
165 |
# -----------------------
|
166 |
# COSTRUZIONE DELL'INTERFACCIA CON GRADIO
|
167 |
def build_interface():
|
168 |
-
"""Costruisce l’interfaccia completa
|
169 |
branches = get_branches()
|
170 |
|
171 |
-
# CSS personalizzato per Smart TV: font e padding maggiorati per una migliore usabilità
|
172 |
css_custom = """
|
173 |
body { font-size: 32px; }
|
174 |
.gradio-container { font-size: 32px; }
|
@@ -177,7 +291,6 @@ def build_interface():
|
|
177 |
.gr-dropdown { font-size: 32px; padding: 10px; }
|
178 |
.gradio-container * { margin: 20px; }
|
179 |
"""
|
180 |
-
# Script JS per navigazione tramite tasti freccia (utile con telecomando)
|
181 |
js_script = """
|
182 |
<script>
|
183 |
document.addEventListener('keydown', function(event) {
|
@@ -196,9 +309,9 @@ def build_interface():
|
|
196 |
</script>
|
197 |
"""
|
198 |
|
199 |
-
with gr.Blocks(css=css_custom, title="Stream4Me
|
200 |
-
gr.Markdown("# Stream4Me - Aggiornamento, Configurazione e
|
201 |
-
gr.HTML(js_script)
|
202 |
with gr.Tabs():
|
203 |
with gr.TabItem("Aggiornamento"):
|
204 |
gr.Markdown("## Aggiorna l'applicazione dal repository GitHub")
|
@@ -224,23 +337,63 @@ def build_interface():
|
|
224 |
refresh_button = gr.Button("Ricarica Impostazioni")
|
225 |
refresh_button.click(fn=get_current_settings, inputs=[], outputs=current_settings_box)
|
226 |
|
227 |
-
with gr.TabItem("Ricerca Film"):
|
228 |
gr.Markdown("## Cerca un Film")
|
229 |
film_input = gr.Textbox(placeholder="Inserisci il nome del film...", label="Nome del Film")
|
230 |
-
|
231 |
-
|
232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
|
234 |
with gr.TabItem("Informazioni"):
|
235 |
gr.Markdown("## Informazioni sul Progetto")
|
236 |
gr.Markdown("""
|
237 |
-
**Stream4Me** è un progetto stand-alone per gestire l’aggiornamento e la
|
238 |
-
completamente indipendente da Kodi e
|
239 |
|
240 |
**Funzionalità:**
|
241 |
- Aggiornamento automatico dal repository GitHub.
|
242 |
- Configurazione personalizzata tramite interfaccia web.
|
243 |
-
- Ricerca
|
|
|
|
|
244 |
- Interfaccia responsive, con navigazione tramite telecomando.
|
245 |
|
246 |
**Repository:** [Stream4Me su GitHub](https://github.com/Stream4me/addon)
|
|
|
1 |
import gradio as gr
|
2 |
+
import requests, zipfile, os, shutil, json, re
|
3 |
|
4 |
# -----------------------
|
5 |
# Free API keys (TMDb e OMDb) ottenute automaticamente
|
|
|
17 |
TMDB_API_KEY = free_keys["tmdb_key"]
|
18 |
|
19 |
# -----------------------
|
20 |
+
# FILE DI CONFIGURAZIONE E VIDEOTECA
|
21 |
SETTINGS_FILE = "settings.json"
|
22 |
+
LIBRARY_FILE = "videoteca.json"
|
23 |
+
|
24 |
DEFAULT_SETTINGS = {
|
25 |
"videolibrarypath": "path/to/videolibrary",
|
26 |
"folder_tvshows": "Serie TV",
|
|
|
63 |
}
|
64 |
return save_settings(new_settings)
|
65 |
|
66 |
+
def load_library():
|
67 |
+
"""Carica la videoteca dal file JSON."""
|
68 |
+
if os.path.exists(LIBRARY_FILE):
|
69 |
+
try:
|
70 |
+
with open(LIBRARY_FILE, "r", encoding="utf-8") as f:
|
71 |
+
return json.load(f)
|
72 |
+
except Exception:
|
73 |
+
return []
|
74 |
+
else:
|
75 |
+
return []
|
76 |
+
|
77 |
+
def save_library(lib):
|
78 |
+
"""Salva la videoteca nel file JSON."""
|
79 |
+
with open(LIBRARY_FILE, "w", encoding="utf-8") as f:
|
80 |
+
json.dump(lib, f, indent=4, ensure_ascii=False)
|
81 |
+
|
82 |
+
def add_to_library(item_str: str, item_type: str) -> str:
|
83 |
+
"""
|
84 |
+
Aggiunge un elemento (film o serie TV) alla videoteca.
|
85 |
+
L'elemento è una stringa formattata con [ID: ...].
|
86 |
+
"""
|
87 |
+
lib = load_library()
|
88 |
+
match = re.search(r'\[ID: (\d+)\]', item_str)
|
89 |
+
if not match:
|
90 |
+
return "Impossibile estrarre l'ID dall'elemento."
|
91 |
+
item_id = match.group(1)
|
92 |
+
# Controlla duplicati
|
93 |
+
for item in lib:
|
94 |
+
if item["id"] == item_id and item["type"] == item_type:
|
95 |
+
return "Elemento già presente in videoteca."
|
96 |
+
new_item = {"id": item_id, "title": item_str, "type": item_type}
|
97 |
+
lib.append(new_item)
|
98 |
+
save_library(lib)
|
99 |
+
return "Elemento aggiunto in videoteca."
|
100 |
+
|
101 |
+
def get_library_items(item_type: str) -> list:
|
102 |
+
"""Restituisce una lista di elementi della videoteca filtrata per tipo."""
|
103 |
+
lib = load_library()
|
104 |
+
return [item["title"] for item in lib if item["type"] == item_type]
|
105 |
+
|
106 |
# -----------------------
|
107 |
# FUNZIONALITÀ DI AGGIORNAMENTO DELL'APPLICAZIONE
|
108 |
def get_branches():
|
|
|
127 |
def update_from_zip(branch: str) -> str:
|
128 |
"""
|
129 |
Scarica il file zip del branch selezionato da GitHub,
|
130 |
+
lo estrae in una cartella target ("s4me_app") e restituisce un log.
|
|
|
131 |
"""
|
132 |
log = []
|
133 |
try:
|
|
|
142 |
f.write(chunk)
|
143 |
log.append("Download completato.")
|
144 |
|
|
|
145 |
extract_base = f"s4me_app_{branch}"
|
146 |
target_dir = "s4me_app"
|
147 |
|
|
|
148 |
if os.path.isdir(target_dir):
|
149 |
shutil.rmtree(target_dir)
|
150 |
log.append(f"Vecchia cartella '{target_dir}' rimossa.")
|
|
|
154 |
zip_ref.extractall(extract_base)
|
155 |
log.append("Estrazione completata.")
|
156 |
|
|
|
157 |
extracted_folder = os.path.join(extract_base, f"addon-{branch}")
|
158 |
if os.path.isdir(extracted_folder):
|
159 |
os.rename(extracted_folder, target_dir)
|
|
|
161 |
else:
|
162 |
log.append("Errore: cartella estratta non trovata.")
|
163 |
|
|
|
164 |
os.remove(zip_filename)
|
165 |
shutil.rmtree(extract_base)
|
166 |
log.append("Pulizia completata. Aggiornamento eseguito con successo.")
|
|
|
173 |
return update_from_zip(selected_branch)
|
174 |
|
175 |
# -----------------------
|
176 |
+
# FUNZIONALITÀ DI RICERCA E STREAMING PER FILM
|
177 |
+
def search_movie_list(movie_title: str):
|
178 |
"""
|
179 |
+
Cerca film tramite TMDb e restituisce una lista per il dropdown.
|
180 |
+
Formato: "Titolo (Data) [ID: movie_id]".
|
181 |
"""
|
182 |
if not movie_title:
|
183 |
+
return []
|
184 |
url = f"https://api.themoviedb.org/3/search/movie?api_key={TMDB_API_KEY}&query={movie_title}"
|
185 |
try:
|
186 |
response = requests.get(url)
|
187 |
response.raise_for_status()
|
188 |
data = response.json()
|
189 |
results = data.get("results", [])
|
190 |
+
choices = []
|
|
|
|
|
191 |
for movie in results:
|
192 |
title = movie.get("title", "Sconosciuto")
|
193 |
release_date = movie.get("release_date", "Data sconosciuta")
|
194 |
+
movie_id = movie.get("id")
|
195 |
+
choices.append(f"{title} ({release_date}) [ID: {movie_id}]")
|
196 |
+
return choices
|
197 |
+
except Exception as e:
|
198 |
+
return [f"Errore durante la ricerca: {str(e)}"]
|
199 |
+
|
200 |
+
def get_streaming_providers(movie_choice: str) -> str:
|
201 |
+
"""
|
202 |
+
Data la scelta del film (contenente l'ID),
|
203 |
+
richiama l'endpoint TMDb per ottenere i provider streaming.
|
204 |
+
"""
|
205 |
+
match = re.search(r'\[ID: (\d+)\]', movie_choice)
|
206 |
+
if not match:
|
207 |
+
return "Impossibile estrarre l'ID del film."
|
208 |
+
movie_id = match.group(1)
|
209 |
+
url = f"https://api.themoviedb.org/3/movie/{movie_id}/watch/providers?api_key={TMDB_API_KEY}"
|
210 |
+
try:
|
211 |
+
response = requests.get(url)
|
212 |
+
response.raise_for_status()
|
213 |
+
data = response.json()
|
214 |
+
results = data.get("results", {})
|
215 |
+
if "IT" in results:
|
216 |
+
providers = results["IT"].get("flatrate", [])
|
217 |
+
elif "US" in results:
|
218 |
+
providers = results["US"].get("flatrate", [])
|
219 |
+
else:
|
220 |
+
providers = []
|
221 |
+
if not providers:
|
222 |
+
return "Nessun canale streaming diretto trovato."
|
223 |
+
output = [provider.get("provider_name", "Sconosciuto") for provider in providers]
|
224 |
+
return "Canali streaming trovati:\n" + "\n".join(output)
|
225 |
except Exception as e:
|
226 |
+
return f"Errore durante la ricerca dei canali: {str(e)}"
|
227 |
+
|
228 |
+
# -----------------------
|
229 |
+
# FUNZIONALITÀ DI RICERCA PER SERIE TV
|
230 |
+
def search_tv_list(tv_title: str):
|
231 |
+
"""
|
232 |
+
Cerca serie TV tramite TMDb e restituisce una lista per il dropdown.
|
233 |
+
Formato: "Nome Serie (Data) [ID: tv_id]".
|
234 |
+
"""
|
235 |
+
if not tv_title:
|
236 |
+
return []
|
237 |
+
url = f"https://api.themoviedb.org/3/search/tv?api_key={TMDB_API_KEY}&query={tv_title}"
|
238 |
+
try:
|
239 |
+
response = requests.get(url)
|
240 |
+
response.raise_for_status()
|
241 |
+
data = response.json()
|
242 |
+
results = data.get("results", [])
|
243 |
+
choices = []
|
244 |
+
for tv in results:
|
245 |
+
name = tv.get("name", "Sconosciuto")
|
246 |
+
first_air_date = tv.get("first_air_date", "Data sconosciuta")
|
247 |
+
tv_id = tv.get("id")
|
248 |
+
choices.append(f"{name} ({first_air_date}) [ID: {tv_id}]")
|
249 |
+
return choices
|
250 |
+
except Exception as e:
|
251 |
+
return [f"Errore durante la ricerca: {str(e)}"]
|
252 |
+
|
253 |
+
def load_tv_episodes(tv_series_choice: str) -> str:
|
254 |
+
"""
|
255 |
+
Data la serie TV selezionata (con l'ID incluso), carica gli episodi della stagione 1.
|
256 |
+
Restituisce una lista formattata degli episodi.
|
257 |
+
"""
|
258 |
+
match = re.search(r'\[ID: (\d+)\]', tv_series_choice)
|
259 |
+
if not match:
|
260 |
+
return "Impossibile estrarre l'ID della serie TV."
|
261 |
+
tv_id = match.group(1)
|
262 |
+
url = f"https://api.themoviedb.org/3/tv/{tv_id}/season/1?api_key={TMDB_API_KEY}"
|
263 |
+
try:
|
264 |
+
response = requests.get(url)
|
265 |
+
response.raise_for_status()
|
266 |
+
data = response.json()
|
267 |
+
episodes = data.get("episodes", [])
|
268 |
+
if not episodes:
|
269 |
+
return "Nessun episodio trovato per la stagione 1."
|
270 |
+
output = []
|
271 |
+
for ep in episodes:
|
272 |
+
ep_num = ep.get("episode_number")
|
273 |
+
name = ep.get("name", "Sconosciuto")
|
274 |
+
overview = ep.get("overview", "")
|
275 |
+
output.append(f"Episodio {ep_num}: {name}\n{overview}\n")
|
276 |
+
return "\n".join(output)
|
277 |
+
except Exception as e:
|
278 |
+
return f"Errore durante il caricamento degli episodi: {str(e)}"
|
279 |
|
280 |
# -----------------------
|
281 |
# COSTRUZIONE DELL'INTERFACCIA CON GRADIO
|
282 |
def build_interface():
|
283 |
+
"""Costruisce l’interfaccia completa con più tab."""
|
284 |
branches = get_branches()
|
285 |
|
|
|
286 |
css_custom = """
|
287 |
body { font-size: 32px; }
|
288 |
.gradio-container { font-size: 32px; }
|
|
|
291 |
.gr-dropdown { font-size: 32px; padding: 10px; }
|
292 |
.gradio-container * { margin: 20px; }
|
293 |
"""
|
|
|
294 |
js_script = """
|
295 |
<script>
|
296 |
document.addEventListener('keydown', function(event) {
|
|
|
309 |
</script>
|
310 |
"""
|
311 |
|
312 |
+
with gr.Blocks(css=css_custom, title="Stream4Me per Smart TV") as demo:
|
313 |
+
gr.Markdown("# Stream4Me - Aggiornamento, Configurazione, Ricerca e Videoteca")
|
314 |
+
gr.HTML(js_script)
|
315 |
with gr.Tabs():
|
316 |
with gr.TabItem("Aggiornamento"):
|
317 |
gr.Markdown("## Aggiorna l'applicazione dal repository GitHub")
|
|
|
337 |
refresh_button = gr.Button("Ricarica Impostazioni")
|
338 |
refresh_button.click(fn=get_current_settings, inputs=[], outputs=current_settings_box)
|
339 |
|
340 |
+
with gr.TabItem("Ricerca Film & Streaming"):
|
341 |
gr.Markdown("## Cerca un Film")
|
342 |
film_input = gr.Textbox(placeholder="Inserisci il nome del film...", label="Nome del Film")
|
343 |
+
film_search_button = gr.Button("Cerca Film")
|
344 |
+
film_dropdown = gr.Dropdown(choices=[], label="Seleziona il Film")
|
345 |
+
film_search_button.click(fn=search_movie_list, inputs=film_input, outputs=film_dropdown)
|
346 |
+
gr.Markdown("### Streaming / Aggiungi in Videoteca")
|
347 |
+
film_stream_button = gr.Button("Guarda Film (Streaming)")
|
348 |
+
film_stream_results = gr.Textbox(label="Risultati Streaming", lines=10)
|
349 |
+
film_stream_button.click(fn=get_streaming_providers, inputs=film_dropdown, outputs=film_stream_results)
|
350 |
+
film_add_button = gr.Button("Aggiungi Film in Videoteca")
|
351 |
+
film_add_status = gr.Textbox(label="Stato Aggiunta", lines=2)
|
352 |
+
film_add_button.click(fn=lambda x: add_to_library(x, "film"), inputs=film_dropdown, outputs=film_add_status)
|
353 |
+
|
354 |
+
with gr.TabItem("Ricerca Serie TV & Videoteca"):
|
355 |
+
gr.Markdown("## Cerca una Serie TV")
|
356 |
+
tv_input = gr.Textbox(placeholder="Inserisci il nome della serie TV...", label="Nome Serie TV")
|
357 |
+
tv_search_button = gr.Button("Cerca Serie TV")
|
358 |
+
tv_dropdown = gr.Dropdown(choices=[], label="Seleziona la Serie TV")
|
359 |
+
tv_search_button.click(fn=search_tv_list, inputs=tv_input, outputs=tv_dropdown)
|
360 |
+
gr.Markdown("### Aggiungi Serie TV in Videoteca")
|
361 |
+
tv_add_button = gr.Button("Aggiungi Serie TV in Videoteca")
|
362 |
+
tv_add_status = gr.Textbox(label="Stato Aggiunta", lines=2)
|
363 |
+
tv_add_button.click(fn=lambda x: add_to_library(x, "tv"), inputs=tv_dropdown, outputs=tv_add_status)
|
364 |
+
|
365 |
+
with gr.TabItem("Videoteca"):
|
366 |
+
gr.Markdown("## Film in Videoteca")
|
367 |
+
film_library_dropdown = gr.Dropdown(choices=get_library_items("film"), label="Film in Videoteca")
|
368 |
+
film_library_button = gr.Button("Guarda Film (Streaming)")
|
369 |
+
film_library_results = gr.Textbox(label="Streaming Film", lines=10)
|
370 |
+
film_library_button.click(fn=get_streaming_providers, inputs=film_library_dropdown, outputs=film_library_results)
|
371 |
+
|
372 |
+
gr.Markdown("## Serie TV in Videoteca")
|
373 |
+
tv_library_dropdown = gr.Dropdown(choices=get_library_items("tv"), label="Serie TV in Videoteca")
|
374 |
+
tv_library_button = gr.Button("Carica Episodi (Stagione 1)")
|
375 |
+
tv_library_results = gr.Textbox(label="Episodi", lines=10)
|
376 |
+
tv_library_button.click(fn=load_tv_episodes, inputs=tv_library_dropdown, outputs=tv_library_results)
|
377 |
+
|
378 |
+
gr.Markdown("### Ricarica Videoteca")
|
379 |
+
refresh_library_button = gr.Button("Ricarica Lista Videoteca")
|
380 |
+
def refresh_library():
|
381 |
+
return get_library_items("film"), get_library_items("tv")
|
382 |
+
film_lib_out, tv_lib_out = gr.Dropdown(choices=[], label=""), gr.Dropdown(choices=[], label="")
|
383 |
+
refresh_library_button.click(fn=refresh_library, inputs=[], outputs=[film_library_dropdown, tv_library_dropdown])
|
384 |
|
385 |
with gr.TabItem("Informazioni"):
|
386 |
gr.Markdown("## Informazioni sul Progetto")
|
387 |
gr.Markdown("""
|
388 |
+
**Stream4Me** è un progetto stand-alone per gestire l’aggiornamento, la configurazione, la ricerca e la videoteca di film e serie TV,
|
389 |
+
completamente indipendente da Kodi e ottimizzato per Smart TV.
|
390 |
|
391 |
**Funzionalità:**
|
392 |
- Aggiornamento automatico dal repository GitHub.
|
393 |
- Configurazione personalizzata tramite interfaccia web.
|
394 |
+
- Ricerca film e serie TV tramite TMDb.
|
395 |
+
- Aggiunta in videoteca e visualizzazione dei provider streaming per i film.
|
396 |
+
- Caricamento degli episodi (es. stagione 1) per le serie TV, per poterle guardare in sequenza.
|
397 |
- Interfaccia responsive, con navigazione tramite telecomando.
|
398 |
|
399 |
**Repository:** [Stream4Me su GitHub](https://github.com/Stream4me/addon)
|