CineMax commited on
Commit
182cdfb
·
verified ·
1 Parent(s): 5313a10

Upload 11 files

Browse files
Files changed (12) hide show
  1. .gitattributes +5 -0
  2. 2 audios.bat +247 -0
  3. New Text Document.bat +152 -0
  4. c.exe +3 -0
  5. c.py +684 -0
  6. ffmpeg.exe +3 -0
  7. ffplay.exe +3 -0
  8. ffprobe.exe +3 -0
  9. job.bat +168 -0
  10. job.exe +3 -0
  11. job.py +836 -0
  12. selecciona segundo audio.bat +51 -0
.gitattributes CHANGED
@@ -61,3 +61,8 @@ TR0N_4res_(2025)_1M4X_Web-Dl_1080p/TR0N_4res_(2025)_1M4X_Web-Dl_1080p_all_audio.
61
  TR0N_4res_(2025)_1M4X_Web-Dl_1080p/TR0N_4res_(2025)_1M4X_Web-Dl_1080p_audio_3.mp4 filter=lfs diff=lfs merge=lfs -text
62
  4lm4_en_ll4m4s_(2025)_Web-Dl_1080p/4lm4_en_ll4m4s_(2025)_Web-Dl_1080p_all_audio.mp4 filter=lfs diff=lfs merge=lfs -text
63
  4lm4_en_ll4m4s_(2025)_Web-Dl_1080p/4lm4_en_ll4m4s_(2025)_Web-Dl_1080p_audio_1.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
61
  TR0N_4res_(2025)_1M4X_Web-Dl_1080p/TR0N_4res_(2025)_1M4X_Web-Dl_1080p_audio_3.mp4 filter=lfs diff=lfs merge=lfs -text
62
  4lm4_en_ll4m4s_(2025)_Web-Dl_1080p/4lm4_en_ll4m4s_(2025)_Web-Dl_1080p_all_audio.mp4 filter=lfs diff=lfs merge=lfs -text
63
  4lm4_en_ll4m4s_(2025)_Web-Dl_1080p/4lm4_en_ll4m4s_(2025)_Web-Dl_1080p_audio_1.mp4 filter=lfs diff=lfs merge=lfs -text
64
+ c.exe filter=lfs diff=lfs merge=lfs -text
65
+ ffmpeg.exe filter=lfs diff=lfs merge=lfs -text
66
+ ffplay.exe filter=lfs diff=lfs merge=lfs -text
67
+ ffprobe.exe filter=lfs diff=lfs merge=lfs -text
68
+ job.exe filter=lfs diff=lfs merge=lfs -text
2 audios.bat ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+
4
+ REM Verifica si se proporcionó un archivo como argumento
5
+ if "%~1"=="" (
6
+ echo No se ha proporcionado ningun archivo. Usa el script de la siguiente manera:
7
+ echo script.bat nombre_del_archivo.ext
8
+ pause
9
+ exit /b 1
10
+ )
11
+
12
+ REM Obtiene el nombre del archivo sin la extensión
13
+ set "filename=%~n1"
14
+ REM Obtiene la extensión del archivo
15
+ set "extension=%~x1"
16
+
17
+ REM Verifica si el archivo existe
18
+ if not exist "%filename%%extension%" (
19
+ echo El archivo "%filename%%extension%" no existe.
20
+ pause
21
+ exit /b 1
22
+ )
23
+
24
+ echo.
25
+ echo ============================================
26
+ echo Archivo "%filename%%extension%" encontrado.
27
+ echo ============================================
28
+ echo.
29
+
30
+ REM Crear carpeta de salida con el nombre del archivo
31
+ set "output_dir=%~dp0%filename%_hls"
32
+ if not exist "!output_dir!" mkdir "!output_dir!"
33
+
34
+ echo Carpeta de salida: %filename%_hls
35
+ echo.
36
+
37
+ REM Detectar todas las pistas de audio
38
+ echo Detectando pistas de audio...
39
+ echo.
40
+
41
+ set audio_count=0
42
+ for /f "tokens=1,2,3 delims=|" %%a in ('ffprobe -v error -select_streams a -show_entries stream^=index:stream_tags^=language^,title -of csv^=p^=0 "%filename%%extension%" 2^>^&1') do (
43
+ set /a audio_count+=1
44
+ set "audio_index_!audio_count!=%%a"
45
+ set "audio_lang_!audio_count!=%%b"
46
+ set "audio_title_!audio_count!=%%c"
47
+
48
+ if "!audio_lang_%%audio_count%%!"=="" set "audio_lang_!audio_count!=und"
49
+ if "!audio_title_%%audio_count%%!"=="" set "audio_title_!audio_count!=Audio_!audio_count!"
50
+
51
+ echo [!audio_count!] Audio - Idioma: !audio_lang_%%audio_count%%! - Titulo: !audio_title_%%audio_count%%!
52
+ )
53
+
54
+ if !audio_count! EQU 0 (
55
+ echo No se encontraron pistas de audio en el video.
56
+ pause
57
+ exit /b 1
58
+ )
59
+
60
+ echo.
61
+ echo Total de audios encontrados: !audio_count!
62
+ echo.
63
+
64
+ REM Detectar todos los subtítulos
65
+ echo Detectando subtitulos...
66
+ echo.
67
+
68
+ set subtitle_count=0
69
+ for /f "tokens=1,2,3 delims=|" %%a in ('ffprobe -v error -select_streams s -show_entries stream^=index:stream_tags^=language^,title -of csv^=p^=0 "%filename%%extension%" 2^>^&1') do (
70
+ set /a subtitle_count+=1
71
+ set "sub_index_!subtitle_count!=%%a"
72
+ set "sub_lang_!subtitle_count!=%%b"
73
+ set "sub_title_!subtitle_count!=%%c"
74
+
75
+ if "!sub_lang_%%subtitle_count%%!"=="" set "sub_lang_!subtitle_count!=und"
76
+ if "!sub_title_%%subtitle_count%%!"=="" set "sub_title_!subtitle_count!=Subtitle_!subtitle_count!"
77
+
78
+ echo [!subtitle_count!] Subtitulo - Idioma: !sub_lang_%%subtitle_count%%! - Titulo: !sub_title_%%subtitle_count%%!
79
+ )
80
+
81
+ echo.
82
+ echo Total de subtitulos encontrados: !subtitle_count!
83
+ echo.
84
+
85
+ REM Generar HLS para cada pista de audio
86
+ echo ============================================
87
+ echo Generando archivos HLS para cada audio...
88
+ echo ============================================
89
+ echo.
90
+
91
+ for /L %%i in (1,1,!audio_count!) do (
92
+ set /a audio_stream_index=%%i-1
93
+ set "audio_name=!audio_title_%%i: =_!"
94
+ set "audio_name=!audio_name:(=!"
95
+ set "audio_name=!audio_name:)=!"
96
+ set "audio_m3u8=!output_dir!\audio_!audio_name!_!audio_lang_%%i!.m3u8"
97
+
98
+ echo [%%i/!audio_count!] Generando HLS para: !audio_title_%%i! ^(!audio_lang_%%i!^)
99
+
100
+ ffmpeg -i "%filename%%extension%" -vn -map 0:a:!audio_stream_index! -c:a mp3 -b:a 192k -hls_time 10 -hls_list_size 0 -hls_segment_filename "!output_dir!\audio_!audio_name!_!audio_lang_%%i!_%%03d.ts" "!audio_m3u8!" -y 2>&1
101
+
102
+ if !ERRORLEVEL! NEQ 0 (
103
+ echo [ERROR] Fallo al generar audio: !audio_title_%%i!
104
+ ) else (
105
+ echo [OK] Audio generado: audio_!audio_name!_!audio_lang_%%i!.m3u8
106
+ )
107
+ echo.
108
+ )
109
+
110
+ REM Convertir subtítulos a WebVTT y generar HLS
111
+ if !subtitle_count! GTR 0 (
112
+ echo ============================================
113
+ echo Generando archivos VTT para subtitulos...
114
+ echo ============================================
115
+ echo.
116
+
117
+ for /L %%i in (1,1,!subtitle_count!) do (
118
+ set /a sub_stream_index=%%i-1
119
+ set "sub_name=!sub_title_%%i: =_!"
120
+ set "sub_name=!sub_name:(=!"
121
+ set "sub_name=!sub_name:)=!"
122
+ set "sub_vtt=!output_dir!\sub_!sub_name!_!sub_lang_%%i!.vtt"
123
+ set "sub_m3u8=!output_dir!\sub_!sub_name!_!sub_lang_%%i!.m3u8"
124
+
125
+ echo [%%i/!subtitle_count!] Extrayendo: !sub_title_%%i! ^(!sub_lang_%%i!^)
126
+
127
+ ffmpeg -i "%filename%%extension%" -map 0:s:!sub_stream_index! "!sub_vtt!" -y 2>&1
128
+
129
+ if !ERRORLEVEL! NEQ 0 (
130
+ echo [ERROR] Fallo al extraer subtitulo: !sub_title_%%i!
131
+ ) else (
132
+ echo [OK] Subtitulo extraido: sub_!sub_name!_!sub_lang_%%i!.vtt
133
+
134
+ REM Crear archivo m3u8 para el subtítulo
135
+ (
136
+ echo #EXTM3U
137
+ echo #EXT-X-VERSION:3
138
+ echo #EXT-X-TARGETDURATION:999999
139
+ echo #EXT-X-MEDIA-SEQUENCE:0
140
+ echo #EXTINF:999999.000000,
141
+ echo sub_!sub_name!_!sub_lang_%%i!.vtt
142
+ echo #EXT-X-ENDLIST
143
+ ) > "!sub_m3u8!"
144
+ echo [OK] M3U8 de subtitulo: sub_!sub_name!_!sub_lang_%%i!.m3u8
145
+ )
146
+ echo.
147
+ )
148
+ )
149
+
150
+ REM Generar HLS para el video
151
+ echo ============================================
152
+ echo Generando HLS para video...
153
+ echo ============================================
154
+ echo.
155
+
156
+ set "video_m3u8=!output_dir!\video.m3u8"
157
+
158
+ ffmpeg -i "%filename%%extension%" -c:v copy -map 0:v:0 -hls_time 10 -hls_list_size 0 -hls_segment_filename "!output_dir!\video_%%03d.ts" "!video_m3u8!" -y 2>&1
159
+
160
+ if !ERRORLEVEL! NEQ 0 (
161
+ echo [ERROR] Fallo al generar video HLS.
162
+ pause
163
+ exit /b 1
164
+ ) else (
165
+ echo [OK] Video generado: video.m3u8
166
+ )
167
+
168
+ echo.
169
+
170
+ REM Generar la lista de reproducción maestro
171
+ echo ============================================
172
+ echo Generando lista de reproduccion maestro...
173
+ echo ============================================
174
+ echo.
175
+
176
+ (
177
+ echo #EXTM3U
178
+ echo #EXT-X-VERSION:4
179
+ echo #EXT-X-INDEPENDENT-SEGMENTS
180
+ echo.
181
+
182
+ REM Agregar todas las pistas de audio
183
+ for /L %%i in (1,1,!audio_count!) do (
184
+ set "audio_name=!audio_title_%%i: =_!"
185
+ set "audio_name=!audio_name:(=!"
186
+ set "audio_name=!audio_name:)=!"
187
+ set "audio_m3u8=audio_!audio_name!_!audio_lang_%%i!.m3u8"
188
+
189
+ if %%i==1 (
190
+ echo #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="!audio_title_%%i!",LANGUAGE="!audio_lang_%%i!",URI="!audio_m3u8!",DEFAULT=YES,AUTOSELECT=YES
191
+ ) else (
192
+ echo #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="!audio_title_%%i!",LANGUAGE="!audio_lang_%%i!",URI="!audio_m3u8!",DEFAULT=NO,AUTOSELECT=NO
193
+ )
194
+ )
195
+
196
+ echo.
197
+
198
+ REM Agregar todos los subtítulos
199
+ if !subtitle_count! GTR 0 (
200
+ for /L %%i in (1,1,!subtitle_count!) do (
201
+ set "sub_name=!sub_title_%%i: =_!"
202
+ set "sub_name=!sub_name:(=!"
203
+ set "sub_name=!sub_name:)=!"
204
+ set "sub_m3u8=sub_!sub_name!_!sub_lang_%%i!.m3u8"
205
+
206
+ if %%i==1 (
207
+ echo #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="!sub_title_%%i!",LANGUAGE="!sub_lang_%%i!",URI="!sub_m3u8!",DEFAULT=YES,AUTOSELECT=YES
208
+ ) else (
209
+ echo #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="!sub_title_%%i!",LANGUAGE="!sub_lang_%%i!",URI="!sub_m3u8!",DEFAULT=NO,AUTOSELECT=NO
210
+ )
211
+ )
212
+ echo.
213
+ )
214
+
215
+ REM Agregar el stream de video
216
+ if !subtitle_count! GTR 0 (
217
+ echo #EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=1920x1080,CODECS="mp4a.40.2,avc1.640028",AUDIO="audio",SUBTITLES="subs"
218
+ ) else (
219
+ echo #EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=1920x1080,CODECS="mp4a.40.2,avc1.640028",AUDIO="audio"
220
+ )
221
+ echo video.m3u8
222
+ ) > "!output_dir!\master.m3u8"
223
+
224
+ if !ERRORLEVEL! NEQ 0 (
225
+ echo [ERROR] Fallo al generar master.m3u8
226
+ pause
227
+ exit /b 1
228
+ )
229
+
230
+ echo [OK] Lista de reproduccion maestro generada: master.m3u8
231
+ echo.
232
+
233
+ echo ============================================
234
+ echo Trabajo completo.
235
+ echo ============================================
236
+ echo.
237
+ echo Todos los archivos generados en: %filename%_hls\
238
+ echo.
239
+ echo Archivos generados:
240
+ echo - master.m3u8 ^(archivo principal^)
241
+ echo - !audio_count! pista^(s^) de audio en formato HLS
242
+ if !subtitle_count! GTR 0 echo - !subtitle_count! subtitulo^(s^) en formato VTT/HLS
243
+ echo - 1 video en formato HLS
244
+ echo.
245
+
246
+ pause
247
+ endlocal
New Text Document.bat ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+
4
+ REM Solicitar la URL del repositorio, el número de archivos de video por lote y el número de archivos de audio a enviar en cada lote
5
+ set /p REPO_URL="Introduce la URL del repositorio (por ejemplo, https://github.com/Armando287/pg.git): "
6
+ set /p videoBatchSize="Introduce el número de archivos de video .ts a enviar en cada lote (por ejemplo, 50 o 100): "
7
+ set /p audioBatchSize="Introduce el número de archivos de audio .ts a enviar en cada lote (por ejemplo, 50 o 100): "
8
+
9
+ set BRANCH=main
10
+ set WORK_DIR=%~dp0
11
+
12
+ REM Cambia al directorio del script
13
+ cd /d "%WORK_DIR%"
14
+
15
+ REM Inicializa el repositorio si no existe
16
+ if not exist .git (
17
+ echo Inicializando el repositorio Git...
18
+ git init
19
+ git remote add origin %REPO_URL%
20
+ git fetch origin %BRANCH%
21
+ git checkout -b %BRANCH%
22
+ )
23
+
24
+ REM Procesar archivos .m3u8
25
+ echo Procesando archivos .m3u8...
26
+ set /a m3u8Counter=0
27
+
28
+ REM Recolectar archivos .m3u8
29
+ for /f "delims=" %%f in ('dir /b /a-d *.m3u8') do (
30
+ set "file=%%f"
31
+ echo Archivo .m3u8 detectado: "!file!"
32
+ git add "!file!"
33
+ set /a m3u8Counter+=1
34
+ )
35
+
36
+ REM Hacer commit y push para archivos .m3u8 si se procesaron
37
+ if %m3u8Counter% gtr 0 (
38
+ echo Haciendo commit y push para archivos .m3u8...
39
+ git commit -m "Subida automática de archivos .m3u8"
40
+ if !ERRORLEVEL! neq 0 (
41
+ echo Error al hacer commit. Error nivel: !ERRORLEVEL!
42
+ exit /b 1
43
+ )
44
+ git push origin %BRANCH%
45
+ if !ERRORLEVEL! neq 0 (
46
+ echo Error al hacer push. Intentando hacer pull para resolver conflictos...
47
+ git pull origin %BRANCH%
48
+ echo Intentando hacer push nuevamente...
49
+ git push origin %BRANCH%
50
+ )
51
+ )
52
+
53
+ REM Procesar archivos .ts con "video" en el nombre en lotes
54
+ echo Procesando archivos .ts con "video" en el nombre en lotes...
55
+ set /a videoCounter=0
56
+ set /a videoBatchCount=0
57
+
58
+ REM Recolectar archivos .ts que contengan "video" en el nombre
59
+ for /f "delims=" %%f in ('dir /b /a-d *video*.ts') do (
60
+ set "file=%%f"
61
+ echo Archivo de video .ts detectado: "!file!"
62
+ git add "!file!"
63
+ set /a videoCounter+=1
64
+
65
+ REM Cada videoBatchSize archivos, hace commit y push
66
+ if !videoCounter! geq %videoBatchSize% (
67
+ echo Haciendo commit y push de %videoBatchSize% archivos de video .ts...
68
+ git commit -m "Subida automática de lote de archivos de video .ts"
69
+ if !ERRORLEVEL! neq 0 (
70
+ echo Error al hacer commit. Error nivel: !ERRORLEVEL!
71
+ exit /b 1
72
+ )
73
+ git push origin %BRANCH%
74
+ if !ERRORLEVEL! neq 0 (
75
+ echo Error al hacer push. Intentando hacer pull para resolver conflictos...
76
+ git pull origin %BRANCH%
77
+ echo Intentando hacer push nuevamente...
78
+ git push origin %BRANCH%
79
+ )
80
+ set /a videoCounter=0
81
+ set /a videoBatchCount+=1
82
+ )
83
+ )
84
+
85
+ REM Realiza commit y push para los archivos de video .ts restantes (si quedan menos de %videoBatchSize%)
86
+ if !videoCounter! gtr 0 (
87
+ echo Haciendo commit y push para los archivos de video .ts restantes...
88
+ git commit -m "Subida automática de los archivos de video .ts restantes"
89
+ if !ERRORLEVEL! neq 0 (
90
+ echo Error al hacer commit. Error nivel: !ERRORLEVEL!
91
+ exit /b 1
92
+ )
93
+ git push origin %BRANCH%
94
+ if !ERRORLEVEL! neq 0 (
95
+ echo Error al hacer push. Intentando hacer pull para resolver conflictos...
96
+ git pull origin %BRANCH%
97
+ echo Intentando hacer push nuevamente...
98
+ git push origin %BRANCH%
99
+ )
100
+ )
101
+
102
+ REM Procesar archivos .ts con "audio" en el nombre en lotes
103
+ echo Procesando archivos .ts con "audio" en el nombre en lotes...
104
+ set /a audioCounter=0
105
+ set /a audioBatchCount=0
106
+
107
+ REM Recolectar archivos .ts que contengan "audio" en el nombre
108
+ for /f "delims=" %%f in ('dir /b /a-d *audio*.ts') do (
109
+ set "file=%%f"
110
+ echo Archivo de audio .ts detectado: "!file!"
111
+ git add "!file!"
112
+ set /a audioCounter+=1
113
+
114
+ REM Cada audioBatchSize archivos, hace commit y push
115
+ if !audioCounter! geq %audioBatchSize% (
116
+ echo Haciendo commit y push de %audioBatchSize% archivos de audio .ts...
117
+ git commit -m "Subida automática de lote de archivos de audio .ts"
118
+ if !ERRORLEVEL! neq 0 (
119
+ echo Error al hacer commit. Error nivel: !ERRORLEVEL!
120
+ exit /b 1
121
+ )
122
+ git push origin %BRANCH%
123
+ if !ERRORLEVEL! neq 0 (
124
+ echo Error al hacer push. Intentando hacer pull para resolver conflictos...
125
+ git pull origin %BRANCH%
126
+ echo Intentando hacer push nuevamente...
127
+ git push origin %BRANCH%
128
+ )
129
+ set /a audioCounter=0
130
+ set /a audioBatchCount+=1
131
+ )
132
+ )
133
+
134
+ REM Realiza commit y push para los archivos de audio .ts restantes (si quedan menos de %audioBatchSize%)
135
+ if !audioCounter! gtr 0 (
136
+ echo Haciendo commit y push para los archivos de audio .ts restantes...
137
+ git commit -m "Subida automática de los archivos de audio .ts restantes"
138
+ if !ERRORLEVEL! neq 0 (
139
+ echo Error al hacer commit. Error nivel: !ERRORLEVEL!
140
+ exit /b 1
141
+ )
142
+ git push origin %BRANCH%
143
+ if !ERRORLEVEL! neq 0 (
144
+ echo Error al hacer push. Intentando hacer pull para resolver conflictos...
145
+ git pull origin %BRANCH%
146
+ echo Intentando hacer push nuevamente...
147
+ git push origin %BRANCH%
148
+ )
149
+ )
150
+
151
+ echo Subida completada.
152
+ pause
c.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b325ae97ae2c94bb2d5368afc7361dbf7f17c649057366bbb089011363c0c576
3
+ size 15264202
c.py ADDED
@@ -0,0 +1,684 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import ttk, filedialog, messagebox
3
+ import subprocess
4
+ import os
5
+ import threading
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+ from urllib.parse import urlparse, unquote
10
+ import platform
11
+ import shutil
12
+ import stat
13
+
14
+ try:
15
+ from tkinterdnd2 import DND_FILES, TkinterDnD
16
+ DRAG_DROP_AVAILABLE = True
17
+ except ImportError:
18
+ DRAG_DROP_AVAILABLE = False
19
+ TkinterDnD = tk
20
+
21
+ try:
22
+ import requests
23
+ HAS_REQUESTS = True
24
+ except ImportError:
25
+ HAS_REQUESTS = False
26
+
27
+ class HLSConverterApp:
28
+ def __init__(self, root):
29
+ self.root = root
30
+ self.root.title("HLS Converter Pro + GitHub Uploader")
31
+ self.root.geometry("1100x900")
32
+ self.root.configure(bg="#121212")
33
+
34
+ # Variables principales
35
+ self.video_file = tk.StringVar()
36
+ self.video_url = tk.StringVar()
37
+ self.input_type = tk.StringVar(value="file")
38
+ self.conversion_mode = tk.StringVar(value="copy_video")
39
+ self.audio_bitrate = tk.StringVar(value="192k")
40
+ self.repo_url = tk.StringVar()
41
+ self.github_token = tk.StringVar()
42
+ self.video_batch_size = tk.IntVar(value=50)
43
+ self.audio_batch_size = tk.IntVar(value=50)
44
+ self.mode = tk.StringVar(value="normal")
45
+ self.delete_local = tk.BooleanVar(value=True)
46
+ self.bulk_sources = []
47
+ self.output_dir = ""
48
+ self.last_output_dir = ""
49
+ self.last_direct_link = ""
50
+ self.processing = False
51
+
52
+ # Progreso
53
+ self.progress_popup = None
54
+ self.step_var = None
55
+ self.percent_var = None
56
+ self.progress_var = None
57
+ self.base_progress = 0.0
58
+ self.progress_span = 100.0
59
+ self.subprocess_flags = subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
60
+
61
+ # Idiomas
62
+ self.lang_names = {
63
+ 'und': 'Principal',
64
+ 'eng': 'English', 'en': 'English',
65
+ 'spa': 'Español', 'es': 'Español',
66
+ 'fra': 'Français', 'fr': 'Français',
67
+ 'deu': 'Deutsch', 'de': 'Deutsch',
68
+ 'ita': 'Italiano', 'it': 'Italiano',
69
+ 'por': 'Português', 'pt': 'Português',
70
+ 'rus': 'Русский', 'ru': 'Русский',
71
+ 'jpn': '日本語', 'ja': '日本語',
72
+ 'kor': '한국어', 'ko': '한국어',
73
+ 'zho': '中文', 'zh': '中文',
74
+ 'ara': 'العربية', 'ar': 'العربية'
75
+ }
76
+
77
+ self.setup_theme()
78
+ self.create_widgets()
79
+ self.github_token.set("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")
80
+
81
+ def setup_theme(self):
82
+ style = ttk.Style()
83
+ style.theme_use('clam')
84
+ style.configure("TFrame", background="#121212")
85
+ style.configure("Card.TFrame", background="#1e1e1e")
86
+ style.configure("TLabel", background="#121212", foreground="#e0e0e0", font=("Segoe UI", 10))
87
+ style.configure("Title.TLabel", font=("Segoe UI", 18, "bold"), foreground="#00d4ff")
88
+ style.configure("TButton", font=("Segoe UI", 10, "bold"))
89
+ style.configure("Accent.TButton", background="#00d4ff", foreground="#000000")
90
+ style.configure("TEntry", fieldbackground="#2d2d2d", foreground="#e0e0e0")
91
+ style.configure("TRadiobutton", background="#121212", foreground="#e0e0e0")
92
+ style.configure("Custom.Horizontal.TProgressbar", thickness=20, background="#00d4ff")
93
+
94
+ def create_widgets(self):
95
+ main_frame = ttk.Frame(self.root, padding="20")
96
+ main_frame.pack(fill=tk.BOTH, expand=True)
97
+
98
+ ttk.Label(main_frame, text="🎬 HLS Converter Pro 2026", style="Title.TLabel").pack(pady=(0, 20))
99
+
100
+ self.notebook = ttk.Notebook(main_frame)
101
+ self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
102
+
103
+ # Tab General
104
+ tab_general = ttk.Frame(self.notebook, padding="10")
105
+ self.notebook.add(tab_general, text="General")
106
+
107
+ token_frame = ttk.LabelFrame(tab_general, text="GitHub Token", padding="15", style="Card.TFrame")
108
+ token_frame.pack(fill=tk.X, pady=(0, 15))
109
+ ttk.Entry(token_frame, textvariable=self.github_token, width=70, show="*").pack(fill=tk.X)
110
+
111
+ mode_frame = ttk.LabelFrame(tab_general, text="Modo", padding="15", style="Card.TFrame")
112
+ mode_frame.pack(fill=tk.X, pady=(0, 15))
113
+ ttk.Radiobutton(mode_frame, text="Normal (un video, repo existente)", variable=self.mode, value="normal", command=self.toggle_mode).pack(anchor=tk.W, pady=3)
114
+ ttk.Radiobutton(mode_frame, text="Bulk (múltiples videos, crea repos automáticamente)", variable=self.mode, value="bulk", command=self.toggle_mode).pack(anchor=tk.W, pady=3)
115
+
116
+ conv_frame = ttk.LabelFrame(tab_general, text="Conversión", padding="15", style="Card.TFrame")
117
+ conv_frame.pack(fill=tk.X, pady=(0, 15))
118
+ ttk.Radiobutton(conv_frame, text="⚡ Copy Video + Copy Audio (más rápido)", variable=self.conversion_mode, value="copy_all").pack(anchor=tk.W, pady=3)
119
+ ttk.Radiobutton(conv_frame, text="⚡ Copy Video + MP3 Audio (recomendado)", variable=self.conversion_mode, value="copy_video").pack(anchor=tk.W, pady=3)
120
+ ttk.Radiobutton(conv_frame, text="🔄 Convertir todo (más lento)", variable=self.conversion_mode, value="convert").pack(anchor=tk.W, pady=3)
121
+
122
+ audio_frame = ttk.Frame(conv_frame)
123
+ audio_frame.pack(fill=tk.X, pady=(10, 0))
124
+ ttk.Label(audio_frame, text="Bitrate MP3:").pack(side=tk.LEFT, padx=(0, 10))
125
+ ttk.Combobox(audio_frame, textvariable=self.audio_bitrate, values=["128k", "192k", "256k", "320k"], state="readonly", width=10).pack(side=tk.LEFT)
126
+
127
+ adv_frame = ttk.LabelFrame(tab_general, text="Avanzado", padding="15", style="Card.TFrame")
128
+ adv_frame.pack(fill=tk.X)
129
+ batch_frame = ttk.Frame(adv_frame)
130
+ batch_frame.pack(fill=tk.X, pady=(0, 10))
131
+ ttk.Label(batch_frame, text="Lote Video:").grid(row=0, column=0, padx=(0, 10))
132
+ ttk.Spinbox(batch_frame, from_=10, to=200, textvariable=self.video_batch_size, width=10).grid(row=0, column=1, padx=(0, 20))
133
+ ttk.Label(batch_frame, text="Lote Audio:").grid(row=0, column=2, padx=(0, 10))
134
+ ttk.Spinbox(batch_frame, from_=10, to=200, textvariable=self.audio_batch_size, width=10).grid(row=0, column=3)
135
+ ttk.Checkbutton(adv_frame, text="Borrar archivos locales después de subir", variable=self.delete_local).pack(anchor=tk.W)
136
+
137
+ # Tab Normal
138
+ tab_normal = ttk.Frame(self.notebook, padding="10")
139
+ self.notebook.add(tab_normal, text="Modo Normal")
140
+
141
+ input_frame = ttk.LabelFrame(tab_normal, text="Fuente", padding="15", style="Card.TFrame")
142
+ input_frame.pack(fill=tk.X, pady=(0, 15))
143
+
144
+ type_frame = ttk.Frame(input_frame)
145
+ type_frame.pack(fill=tk.X, pady=(0, 10))
146
+ ttk.Radiobutton(type_frame, text="📁 Archivo", variable=self.input_type, value="file", command=self.toggle_input).pack(side=tk.LEFT, padx=(0, 20))
147
+ ttk.Radiobutton(type_frame, text="🌐 URL", variable=self.input_type, value="url", command=self.toggle_input).pack(side=tk.LEFT)
148
+
149
+ self.file_frame = ttk.Frame(input_frame)
150
+ ttk.Button(self.file_frame, text="📁 Seleccionar", command=self.browse_file).pack(anchor=tk.W, pady=(0, 10))
151
+ ttk.Label(self.file_frame, textvariable=self.video_file, foreground="#888").pack(fill=tk.X)
152
+
153
+ self.url_frame = ttk.Frame(input_frame)
154
+ ttk.Label(self.url_frame, text="URL del video:").pack(anchor=tk.W, pady=(0, 5))
155
+ ttk.Entry(self.url_frame, textvariable=self.video_url, width=70).pack(fill=tk.X)
156
+
157
+ repo_frame = ttk.LabelFrame(tab_normal, text="Repositorio GitHub (existente)", padding="15", style="Card.TFrame")
158
+ repo_frame.pack(fill=tk.X)
159
+ ttk.Entry(repo_frame, textvariable=self.repo_url, width=70).pack(fill=tk.X)
160
+
161
+ # Tab Bulk
162
+ tab_bulk = ttk.Frame(self.notebook, padding="10")
163
+ self.notebook.add(tab_bulk, text="Modo Bulk")
164
+
165
+ bulk_frame = ttk.LabelFrame(tab_bulk, text="Lista de Videos", padding="15", style="Card.TFrame")
166
+ bulk_frame.pack(fill=tk.BOTH, expand=True)
167
+ ttk.Label(bulk_frame, text="Cada video creará su propio repositorio automáticamente", foreground="#ffff00").pack(anchor=tk.W, pady=(0, 10))
168
+
169
+ self.bulk_list = tk.Listbox(bulk_frame, height=15, bg="#2d2d2d", fg="#e0e0e0")
170
+ self.bulk_list.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
171
+
172
+ btn_bulk = ttk.Frame(bulk_frame)
173
+ btn_bulk.pack(fill=tk.X)
174
+ ttk.Button(btn_bulk, text="+ Archivos", command=self.add_bulk_files).pack(side=tk.LEFT, padx=(0, 5))
175
+ ttk.Button(btn_bulk, text="+ URLs", command=self.add_bulk_urls).pack(side=tk.LEFT, padx=(0, 5))
176
+ ttk.Button(btn_bulk, text="Remover", command=self.remove_bulk).pack(side=tk.LEFT, padx=(0, 5))
177
+ ttk.Button(btn_bulk, text="Limpiar", command=self.clear_bulk).pack(side=tk.LEFT)
178
+
179
+ # Botones principales
180
+ btn_frame = ttk.Frame(main_frame)
181
+ btn_frame.pack(pady=15)
182
+ self.start_btn = ttk.Button(btn_frame, text="🚀 Iniciar", style="Accent.TButton", command=self.start_processing)
183
+ self.start_btn.pack(side=tk.LEFT, padx=(0, 10), ipady=8, ipadx=25)
184
+ self.btn_folder = ttk.Button(btn_frame, text="📂 Abrir Carpeta", command=self.open_folder, state=tk.DISABLED)
185
+ self.btn_folder.pack(side=tk.LEFT, padx=(0, 10), ipady=8, ipadx=20)
186
+ self.btn_link = ttk.Button(btn_frame, text="📋 Copiar Link", command=self.copy_link, state=tk.DISABLED)
187
+ self.btn_link.pack(side=tk.LEFT, ipady=8, ipadx=20)
188
+
189
+ # Log
190
+ log_frame = ttk.LabelFrame(main_frame, text="Registro", padding="15", style="Card.TFrame")
191
+ log_frame.pack(fill=tk.BOTH, expand=True)
192
+ self.log_text = tk.Text(log_frame, height=15, bg="#1e1e1e", fg="#00ff9d", font=("Consolas", 9), wrap=tk.WORD)
193
+ self.log_text.pack(fill=tk.BOTH, expand=True)
194
+
195
+ self.toggle_input()
196
+ self.toggle_mode()
197
+
198
+ def toggle_mode(self):
199
+ if self.mode.get() == "normal":
200
+ self.notebook.select(1)
201
+ else:
202
+ self.notebook.select(2)
203
+
204
+ def toggle_input(self):
205
+ self.file_frame.pack_forget()
206
+ self.url_frame.pack_forget()
207
+ if self.input_type.get() == "file":
208
+ self.file_frame.pack(fill=tk.X)
209
+ else:
210
+ self.url_frame.pack(fill=tk.X)
211
+
212
+ def browse_file(self):
213
+ filename = filedialog.askopenfilename(
214
+ title="Seleccionar video",
215
+ filetypes=[("Videos", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")]
216
+ )
217
+ if filename:
218
+ self.video_file.set(filename)
219
+ self.log(f"✓ Archivo: {os.path.basename(filename)}")
220
+
221
+ def add_bulk_files(self):
222
+ files = filedialog.askopenfilenames(
223
+ title="Seleccionar videos",
224
+ filetypes=[("Videos", "*.mp4 *.mkv *.avi *.mov *.ts"), ("Todos", "*.*")]
225
+ )
226
+ for f in files:
227
+ if f not in [s['value'] for s in self.bulk_sources]:
228
+ self.bulk_sources.append({"type": "file", "value": f})
229
+ self.bulk_list.insert(tk.END, f"Archivo: {os.path.basename(f)}")
230
+
231
+ def add_bulk_urls(self):
232
+ dialog = tk.Toplevel(self.root)
233
+ dialog.title("Añadir URLs")
234
+ dialog.geometry("600x400")
235
+ ttk.Label(dialog, text="URLs (una por línea):").pack(pady=10)
236
+ text = tk.Text(dialog, height=20)
237
+ text.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
238
+ def add():
239
+ lines = [l.strip() for l in text.get("1.0", tk.END).splitlines() if l.strip()]
240
+ for url in lines:
241
+ if url not in [s['value'] for s in self.bulk_sources]:
242
+ self.bulk_sources.append({"type": "url", "value": url})
243
+ self.bulk_list.insert(tk.END, f"URL: {url[:60]}...")
244
+ dialog.destroy()
245
+ ttk.Button(dialog, text="Añadir", command=add).pack(pady=10)
246
+
247
+ def remove_bulk(self):
248
+ selected = self.bulk_list.curselection()
249
+ for i in reversed(selected):
250
+ del self.bulk_sources[i]
251
+ self.bulk_list.delete(i)
252
+
253
+ def clear_bulk(self):
254
+ self.bulk_sources.clear()
255
+ self.bulk_list.delete(0, tk.END)
256
+
257
+ def open_folder(self):
258
+ if self.last_output_dir and os.path.exists(self.last_output_dir):
259
+ if platform.system() == "Windows":
260
+ os.startfile(self.last_output_dir)
261
+ else:
262
+ subprocess.call(["open" if platform.system() == "Darwin" else "xdg-open", self.last_output_dir])
263
+
264
+ def copy_link(self):
265
+ if self.last_direct_link:
266
+ self.root.clipboard_clear()
267
+ self.root.clipboard_append(self.last_direct_link)
268
+ messagebox.showinfo("Copiado", f"Link copiado:\n\n{self.last_direct_link}")
269
+
270
+ def log(self, msg):
271
+ self.log_text.insert(tk.END, f"{msg}\n")
272
+ self.log_text.see(tk.END)
273
+ self.root.update_idletasks()
274
+
275
+ def show_progress(self):
276
+ if self.progress_popup:
277
+ return
278
+ self.step_var = tk.StringVar(value="Iniciando...")
279
+ self.percent_var = tk.StringVar(value="0%")
280
+ self.progress_var = tk.DoubleVar(value=0)
281
+ popup = tk.Toplevel(self.root)
282
+ popup.title("Progreso")
283
+ popup.geometry("500x200")
284
+ popup.configure(bg="#1e1e1e")
285
+ popup.transient(self.root)
286
+ popup.grab_set()
287
+ frame = ttk.Frame(popup, padding="25")
288
+ frame.pack(fill=tk.BOTH, expand=True)
289
+ ttk.Label(frame, textvariable=self.step_var, font=("Segoe UI", 11)).pack(pady=(0, 15))
290
+ ttk.Progressbar(frame, variable=self.progress_var, length=420, style="Custom.Horizontal.TProgressbar").pack(pady=(0, 15))
291
+ ttk.Label(frame, textvariable=self.percent_var, font=("Segoe UI", 16, "bold"), foreground="#00ff9d").pack()
292
+ self.progress_popup = popup
293
+
294
+ def close_progress(self):
295
+ if self.progress_popup:
296
+ self.progress_popup.destroy()
297
+ self.progress_popup = None
298
+
299
+ def update_progress(self, value, step=""):
300
+ if not self.progress_popup:
301
+ return
302
+ overall = self.base_progress + (value / 100.0 * self.progress_span)
303
+ overall = min(100.0, max(0.0, overall))
304
+ self.progress_var.set(overall)
305
+ self.percent_var.set(f"{int(overall)}%")
306
+ if step:
307
+ self.step_var.set(step)
308
+ self.root.update_idletasks()
309
+
310
+ def is_url(self, source):
311
+ return str(source).startswith(('http://', 'https://'))
312
+
313
+ def get_url_headers(self):
314
+ return ['-user_agent', 'Mozilla/5.0', '-headers', 'Referer: https://rumble.com/\r\n']
315
+
316
+ def run_cmd(self, cmd, source=""):
317
+ if self.is_url(source):
318
+ try:
319
+ i = cmd.index('-i')
320
+ cmd = cmd[:i] + self.get_url_headers() + cmd[i:]
321
+ except ValueError:
322
+ pass
323
+ return subprocess.run(cmd, capture_output=True, text=True, creationflags=self.subprocess_flags)
324
+
325
+ def get_duration(self, source):
326
+ try:
327
+ cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', '-i', source]
328
+ result = self.run_cmd(cmd, source)
329
+ if result.returncode == 0 and result.stdout.strip():
330
+ return float(result.stdout.strip())
331
+ except:
332
+ pass
333
+ return None
334
+
335
+ def get_metadata(self, source):
336
+ try:
337
+ cmd = ['ffprobe', '-v', 'error', '-show_format', '-print_format', 'json', '-i', source]
338
+ result = self.run_cmd(cmd, source)
339
+ if result.returncode != 0:
340
+ return None, None
341
+ data = json.loads(result.stdout)
342
+ tags = data.get('format', {}).get('tags', {})
343
+ title = tags.get('title', '').strip() or None
344
+ show = tags.get('show', '').strip() or None
345
+ return title, show
346
+ except:
347
+ return None, None
348
+
349
+ def get_filename_from_url(self, url):
350
+ parsed = urlparse(url)
351
+ path = unquote(parsed.path)
352
+ basename = os.path.basename(path)
353
+ if basename and '.' in basename:
354
+ return Path(basename).stem
355
+ return None
356
+
357
+ def build_name_for_bulk(self, source, is_file):
358
+ if is_file:
359
+ base_name = Path(source).stem
360
+ else:
361
+ base_name = self.get_filename_from_url(source) or "video"
362
+ base_name = re.sub(r'\.(mp4|mkv|avi|mov|ts|webm)$', '', base_name, flags=re.IGNORECASE)
363
+ title, show = self.get_metadata(source)
364
+ parts = [base_name]
365
+ if title:
366
+ title_clean = re.sub(r'[^\w\s\-\(\)]', '_', title)
367
+ title_clean = re.sub(r'\s+', '_', title_clean).strip('_')
368
+ parts.append(title_clean)
369
+ if show:
370
+ show_clean = re.sub(r'[^\w\s\-\(\)]', '_', show)
371
+ show_clean = re.sub(r'\s+', '_', show_clean).strip('_')
372
+ parts.append(show_clean)
373
+ final_name = '_'.join(parts)
374
+ self.log(f"📝 Nombre generado: {final_name}")
375
+ if title:
376
+ self.log(f" • Title: {title}")
377
+ if show:
378
+ self.log(f" • Show: {show}")
379
+ return final_name
380
+
381
+ def clean_for_folder(self, name):
382
+ name = re.sub(r'[<>:"/\\|?*]', '_', name)
383
+ return name.strip()[:200]
384
+
385
+ def clean_for_repo(self, name):
386
+ name = re.sub(r'[^a-zA-Z0-9_-]', '-', name)
387
+ name = re.sub(r'-+', '-', name)
388
+ return name.strip('-')[:100]
389
+
390
+ def get_audio_streams(self, source):
391
+ try:
392
+ cmd = ['ffprobe', '-v', 'error', '-select_streams', 'a', '-show_entries', 'stream=index:stream_tags=language', '-of', 'json', '-i', source]
393
+ result = self.run_cmd(cmd, source)
394
+ data = json.loads(result.stdout)
395
+ streams = []
396
+ for s in data.get('streams', []):
397
+ lang = s.get('tags', {}).get('language', 'und')
398
+ streams.append({'index': s['index'], 'lang': lang})
399
+ return streams
400
+ except:
401
+ return []
402
+
403
+ def convert_audio(self, source, stream_index, output_dir, audio_file, segment_file):
404
+ mode = self.conversion_mode.get()
405
+ if mode == "copy_all":
406
+ codec = ['-c:a', 'copy']
407
+ else:
408
+ codec = ['-c:a', 'libmp3lame', '-b:a', self.audio_bitrate.get()]
409
+ cmd = ['ffmpeg', '-i', source, '-map', f'0:{stream_index}', *codec, '-vn',
410
+ '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(segment_file),
411
+ '-start_number', '1', '-hls_playlist_type', 'vod',
412
+ str(audio_file), '-y', '-loglevel', 'error']
413
+ result = self.run_cmd(cmd, source)
414
+ if result.returncode != 0:
415
+ raise Exception(f"Error audio: {result.stderr}")
416
+
417
+ def convert_video(self, source, output_dir):
418
+ video_file = output_dir / "index-v1-a1.m3u8"
419
+ segment_file = output_dir / "index-v1-a1_%03d.ts"
420
+ if self.conversion_mode.get() in ["copy_all", "copy_video"]:
421
+ codec = ['-c:v', 'copy']
422
+ else:
423
+ codec = ['-c:v', 'libx264', '-preset', 'fast', '-crf', '23']
424
+ cmd = ['ffmpeg', '-i', source, *codec, '-map', '0:v?', '-an',
425
+ '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', str(segment_file),
426
+ '-start_number', '1', '-hls_playlist_type', 'vod',
427
+ str(video_file), '-y', '-loglevel', 'error']
428
+ result = self.run_cmd(cmd, source)
429
+ if result.returncode != 0:
430
+ raise Exception(f"Error video: {result.stderr}")
431
+
432
+ def post_process_playlist(self, path: Path):
433
+ if not path.exists():
434
+ return
435
+ with open(path, 'r', encoding='utf-8') as f:
436
+ lines = f.readlines()
437
+ if not lines or lines[0].strip() != '#EXTM3U':
438
+ return
439
+ new_lines = ['#EXTM3U\n']
440
+ new_lines.append('#EXT-X-ALLOW-CACHE:YES\n')
441
+ new_lines.append('#EXT-X-VERSION:3\n')
442
+ for line in lines[1:]:
443
+ new_lines.append(line)
444
+ with open(path, 'w', encoding='utf-8') as f:
445
+ f.writelines(new_lines)
446
+
447
+ def create_master(self, output_dir, renditions):
448
+ master = output_dir / "master.m3u8"
449
+ with open(master, 'w', encoding='utf-8') as f:
450
+ f.write("#EXTM3U\n")
451
+ for i, rend in enumerate(renditions, 1):
452
+ f.write(f'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="{rend["name"]}",LANGUAGE="{rend["lang"]}",'
453
+ f'AUTOSELECT={rend["autoselect"]},DEFAULT={rend["default"]},CHANNELS="2",URI="index-a{i}.m3u8"\n')
454
+ bandwidth = 1381285
455
+ frame_rate = "24.000"
456
+ codecs = "avc1.64001f,mp3" # MP3
457
+ f.write(f'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH={bandwidth},RESOLUTION=1920x1080,FRAME-RATE={frame_rate},'
458
+ f'CODECS="{codecs}",VIDEO-RANGE=SDR,AUDIO="audio0"\n')
459
+ f.write("index-v1-a1.m3u8\n")
460
+ f.write(f'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH={bandwidth},RESOLUTION=1280x720,FRAME-RATE={frame_rate},'
461
+ f'CODECS="{codecs}",VIDEO-RANGE=SDR,AUDIO="audio0"\n')
462
+ f.write("index-v1-a1.m3u8\n")
463
+
464
+ def create_repo(self, name, token):
465
+ url = "https://api.github.com/user/repos"
466
+ headers = {"Authorization": f"token {token}"}
467
+ data = {"name": name, "private": False}
468
+ response = requests.post(url, headers=headers, json=data, timeout=30)
469
+ if response.status_code == 201:
470
+ repo_data = response.json()
471
+ return repo_data['clone_url'], repo_data['html_url']
472
+ elif response.status_code == 422:
473
+ r = requests.get("https://api.github.com/user", headers=headers)
474
+ username = r.json()['login']
475
+ return f"https://github.com/{username}/{name}.git", f"https://github.com/{username}/{name}"
476
+ else:
477
+ raise Exception(f"Error creando repo: {response.status_code}")
478
+
479
+ def git_upload(self, output_dir, repo_url, token, repo_name):
480
+ git_dir = str(output_dir)
481
+ if '@' not in repo_url:
482
+ remote = repo_url.replace('https://', f'https://{token}@')
483
+ else:
484
+ remote = repo_url
485
+ subprocess.run(['git', 'init'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
486
+ subprocess.run(['git', 'branch', '-M', 'main'], cwd=git_dir, creationflags=self.subprocess_flags)
487
+ subprocess.run(['git', 'remote', 'remove', 'origin'], cwd=git_dir, stderr=subprocess.DEVNULL, creationflags=self.subprocess_flags)
488
+ subprocess.run(['git', 'remote', 'add', 'origin', remote], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
489
+
490
+ files = [f for f in os.listdir(git_dir) if os.path.isfile(os.path.join(git_dir, f))]
491
+ m3u8_files = [f for f in files if f.endswith('.m3u8')]
492
+ video_ts = [f for f in files if f.startswith('index-v1-a1') and f.endswith('.ts')]
493
+ audio_ts = [f for f in files if f.startswith('index-a') and f.endswith('.ts') and not f.startswith('index-v')]
494
+
495
+ if m3u8_files:
496
+ subprocess.run(['git', 'add'] + m3u8_files, cwd=git_dir, check=True, creationflags=self.subprocess_flags)
497
+ subprocess.run(['git', 'commit', '-m', 'Add playlists'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
498
+ subprocess.run(['git', 'push', '-f', 'origin', 'main'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
499
+ self.update_progress(20, "Playlists subidas")
500
+
501
+ if video_ts:
502
+ batch_size = self.video_batch_size.get()
503
+ for i in range(0, len(video_ts), batch_size):
504
+ batch = video_ts[i:i+batch_size]
505
+ subprocess.run(['git', 'add'] + batch, cwd=git_dir, check=True, creationflags=self.subprocess_flags)
506
+ subprocess.run(['git', 'commit', '-m', f'Video batch {i//batch_size + 1}'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
507
+ subprocess.run(['git', 'push', '-f', 'origin', 'main'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
508
+ progress = 20 + (50 * (i + len(batch)) / len(video_ts))
509
+ self.update_progress(progress, f"Video batch {i//batch_size + 1}")
510
+
511
+ if audio_ts:
512
+ batch_size = self.audio_batch_size.get()
513
+ for i in range(0, len(audio_ts), batch_size):
514
+ batch = audio_ts[i:i+batch_size]
515
+ subprocess.run(['git', 'add'] + batch, cwd=git_dir, check=True, creationflags=self.subprocess_flags)
516
+ subprocess.run(['git', 'commit', '-m', f'Audio batch {i//batch_size + 1}'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
517
+ subprocess.run(['git', 'push', '-f', 'origin', 'main'], cwd=git_dir, check=True, creationflags=self.subprocess_flags)
518
+ progress = 70 + (30 * (i + len(batch)) / len(audio_ts))
519
+ self.update_progress(progress, f"Audio batch {i//batch_size + 1}")
520
+
521
+ def process_video(self, source, is_file, is_bulk):
522
+ self.log(f"\n{'='*60}")
523
+ self.log(f"{'📁 ARCHIVO' if is_file else '🌐 URL'}: {os.path.basename(source) if is_file else source[:80]}")
524
+
525
+ if is_bulk:
526
+ output_name = self.build_name_for_bulk(source, is_file)
527
+ else:
528
+ if is_file:
529
+ output_name = Path(source).stem
530
+ else:
531
+ output_name = self.get_filename_from_url(source) or "video"
532
+
533
+ folder_name = self.clean_for_folder(output_name)
534
+ repo_name = self.clean_for_repo(output_name)
535
+
536
+ if is_file:
537
+ base_dir = Path(source).parent
538
+ else:
539
+ base_dir = Path.cwd()
540
+
541
+ self.output_dir = base_dir / f"{folder_name}_hls"
542
+ self.output_dir.mkdir(exist_ok=True)
543
+ self.log(f"📁 Carpeta: {self.output_dir.name}")
544
+
545
+ self.update_progress(5, "Analizando...")
546
+ audio_streams = self.get_audio_streams(source)
547
+ self.log(f"🎵 Audios detectados: {len(audio_streams)}")
548
+
549
+ audio_streams.sort(key=lambda x: 0 if x['lang'] in ['eng', 'en'] else 1)
550
+ renditions = []
551
+ for idx, stream in enumerate(audio_streams):
552
+ lang = stream['lang']
553
+ name = self.lang_names.get(lang, lang.upper())
554
+ if lang == 'und':
555
+ name = 'Principal'
556
+ renditions.append({
557
+ 'name': name,
558
+ 'lang': lang,
559
+ 'default': "YES" if idx == 0 else "NO",
560
+ 'autoselect': "YES" if idx == 0 else "NO",
561
+ 'stream_index': stream['index']
562
+ })
563
+
564
+ if len(renditions) == 1:
565
+ self.log("Solo un audio → añadiendo uno falso duplicado")
566
+ renditions.append({
567
+ 'name': 'Original',
568
+ 'lang': 'und',
569
+ 'default': "NO",
570
+ 'autoselect': "NO",
571
+ 'stream_index': audio_streams[0]['index']
572
+ })
573
+
574
+ self.update_progress(10, "Convirtiendo audios...")
575
+ for i, rend in enumerate(renditions, 1):
576
+ audio_file = self.output_dir / f"index-a{i}.m3u8"
577
+ segment_file = self.output_dir / f"index-a{i}_%03d.ts"
578
+ self.convert_audio(source, rend['stream_index'], self.output_dir, audio_file, segment_file)
579
+
580
+ self.update_progress(50, "Convirtiendo video...")
581
+ self.convert_video(source, self.output_dir)
582
+
583
+ self.update_progress(75, "Ajustando playlists...")
584
+ for playlist in self.output_dir.glob("index-*.m3u8"):
585
+ self.post_process_playlist(playlist)
586
+
587
+ self.update_progress(80, "Creando master.m3u8...")
588
+ self.create_master(self.output_dir, renditions)
589
+
590
+ self.update_progress(85, "Subiendo a GitHub...")
591
+ token = self.github_token.get().strip()
592
+ if is_bulk:
593
+ clone_url, html_url = self.create_repo(repo_name, token)
594
+ self.log(f"✓ Repo creado: {html_url}")
595
+ else:
596
+ repo_url = self.repo_url.get().strip()
597
+ if not repo_url.endswith('.git'):
598
+ repo_url += '.git'
599
+ clone_url = repo_url
600
+ html_url = repo_url.replace('.git', '')
601
+
602
+ self.git_upload(self.output_dir, clone_url, token, repo_name)
603
+
604
+ raw_url = html_url.replace('github.com', 'raw.githubusercontent.com') + '/main/master.m3u8'
605
+ self.last_direct_link = raw_url
606
+
607
+ self.log(f"\n{'='*60}")
608
+ self.log(f"✅ COMPLETADO")
609
+ self.log(f"📦 Repo: {html_url}")
610
+ self.log(f"🎬 Link: {raw_url}")
611
+ self.log(f"{'='*60}\n")
612
+
613
+ if self.delete_local.get():
614
+ try:
615
+ shutil.rmtree(self.output_dir, onerror=lambda func, path, exc: (os.chmod(path, stat.S_IWRITE), func(path)))
616
+ self.log("🗑️ Archivos locales borrados")
617
+ except:
618
+ pass
619
+
620
+ self.last_output_dir = str(self.output_dir)
621
+ self.update_progress(100, "Completado")
622
+
623
+ def start_processing(self):
624
+ if self.processing:
625
+ return
626
+ token = self.github_token.get().strip()
627
+ if not token:
628
+ messagebox.showerror("Error", "Token de GitHub requerido")
629
+ return
630
+ self.processing = True
631
+ self.start_btn.config(state=tk.DISABLED)
632
+ self.show_progress()
633
+
634
+ def process():
635
+ try:
636
+ if self.mode.get() == "normal":
637
+ if self.input_type.get() == "file":
638
+ source = self.video_file.get()
639
+ if not source or not os.path.exists(source):
640
+ raise Exception("Selecciona un archivo válido")
641
+ is_file = True
642
+ else:
643
+ source = self.video_url.get().strip()
644
+ if not source:
645
+ raise Exception("Ingresa una URL válida")
646
+ is_file = False
647
+ if not self.repo_url.get().strip():
648
+ raise Exception("Ingresa URL del repositorio")
649
+ self.process_video(source, is_file, False)
650
+ else:
651
+ if not self.bulk_sources:
652
+ raise Exception("Agrega videos en modo bulk")
653
+ if not HAS_REQUESTS:
654
+ raise Exception("Instala 'requests' para modo bulk")
655
+ total = len(self.bulk_sources)
656
+ for idx, item in enumerate(self.bulk_sources, 1):
657
+ self.log(f"\n{'='*60}")
658
+ self.log(f"VIDEO {idx}/{total}")
659
+ self.log(f"{'='*60}")
660
+ source = item['value']
661
+ is_file = item['type'] == 'file'
662
+ self.base_progress = ((idx - 1) / total) * 100
663
+ self.progress_span = 100 / total
664
+ try:
665
+ self.process_video(source, is_file, True)
666
+ except Exception as e:
667
+ self.log(f"❌ Error: {str(e)}")
668
+ self.log(f"\n🎉 BULK COMPLETADO: {total} videos procesados")
669
+ self.root.after(0, lambda: self.btn_folder.config(state=tk.NORMAL))
670
+ self.root.after(0, lambda: self.btn_link.config(state=tk.NORMAL))
671
+ except Exception as e:
672
+ self.log(f"\n❌ ERROR: {str(e)}")
673
+ self.root.after(0, lambda msg=str(e): messagebox.showerror("Error", msg))
674
+ finally:
675
+ self.processing = False
676
+ self.root.after(0, lambda: self.start_btn.config(state=tk.NORMAL))
677
+ self.close_progress()
678
+
679
+ threading.Thread(target=process, daemon=True).start()
680
+
681
+ if __name__ == "__main__":
682
+ root = TkinterDnD.Tk() if DRAG_DROP_AVAILABLE else tk.Tk()
683
+ app = HLSConverterApp(root)
684
+ root.mainloop()
ffmpeg.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ea6d3cb3c45291efdfbf7c6f08a0e7bb95718ad4622a15839067bc31afa674e
3
+ size 121986048
ffplay.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a71d514a6881a79dced3af89b5cca7589179be52c8e5c8aadafedd6575f94dda
3
+ size 121810944
ffprobe.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:569c668c848ab9b64369d93a27c56e850a6627bd9966fece8e370fb10fe176a2
3
+ size 121816064
job.bat ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+
4
+ :: Obt�n el directorio donde est� ubicado este script
5
+ set "script_dir=%~dp0"
6
+
7
+ :: Verifica que se han pasado archivos como argumentos
8
+ if "%~1"=="" (
9
+ echo Por favor, pasa al menos un archivo MKV como argumento.
10
+ pause
11
+ exit /b 1
12
+ )
13
+
14
+ :: Procesa cada archivo MKV pasado como argumento
15
+ :loop
16
+ if "%~1"=="" goto endloop
17
+
18
+ :: Verifica la extensi�n del archivo
19
+ if /i not "%~x1"==".mkv" (
20
+ echo El archivo "%~1" no es un archivo MKV.
21
+ shift
22
+ goto loop
23
+ )
24
+
25
+ set "input_file=%~1"
26
+ set "video_name=%~n1"
27
+
28
+ echo.
29
+ echo ============================================
30
+ echo Procesando: "%video_name%"
31
+ echo ============================================
32
+ echo.
33
+
34
+ :: PASO 1: Detectar todos los audios del video
35
+ echo Detectando pistas de audio...
36
+ echo.
37
+
38
+ ffprobe -v error -select_streams a -show_entries stream=index,codec_name,channels:stream_tags=language,title -of compact=p=0:nk=1 "%input_file%" > "%temp%\audio_info.txt"
39
+
40
+ set audio_count=0
41
+ for /f "usebackq delims=" %%a in ("%temp%\audio_info.txt") do (
42
+ set /a audio_count+=1
43
+ set "audio_line_!audio_count!=%%a"
44
+ echo [!audio_count!] %%a
45
+ )
46
+
47
+ if !audio_count! EQU 0 (
48
+ echo No se encontraron pistas de audio en el video.
49
+ shift
50
+ goto loop
51
+ )
52
+
53
+ echo.
54
+ echo Se encontraron !audio_count! pista^(s^) de audio.
55
+ echo.
56
+
57
+ :: PASO 2: Pedir al usuario que elija un audio
58
+ :ask_audio
59
+ set /p "selected_audio=Elige el numero de audio para el video final (1-!audio_count!): "
60
+
61
+ if !selected_audio! LSS 1 goto ask_audio
62
+ if !selected_audio! GTR !audio_count! goto ask_audio
63
+
64
+ set /a selected_audio_index=!selected_audio!-1
65
+
66
+ echo.
67
+ echo Audio seleccionado: [!selected_audio!]
68
+ echo.
69
+
70
+ :: PASO 3: Crear nombre de carpeta con transformaciones (a=4, i=1, o=0)
71
+ set "folder_name=!video_name!"
72
+ set "folder_name=!folder_name:a=4!"
73
+ set "folder_name=!folder_name:A=4!"
74
+ set "folder_name=!folder_name:i=1!"
75
+ set "folder_name=!folder_name:I=1!"
76
+ set "folder_name=!folder_name:o=0!"
77
+ set "folder_name=!folder_name:O=0!"
78
+ set "folder_name=!folder_name: =_!"
79
+
80
+ set "output_dir=%script_dir%!folder_name!"
81
+
82
+ :: Crear el directorio de salida
83
+ if not exist "!output_dir!" mkdir "!output_dir!"
84
+
85
+ echo Carpeta de salida: !folder_name!
86
+ echo.
87
+
88
+ :: PASO 4: Convertir TODOS los audios a FLAC + video copy
89
+ set "all_audio_file=!output_dir!\!folder_name!_all_audio.mp4"
90
+ echo Convirtiendo todos los audios a FLAC con video copy...
91
+
92
+ ffmpeg -i "%input_file%" -map 0:v -c:v copy -map 0:a -c:a flac -compression_level 0 -metadata title="power-prods" -metadata comment="power-prods" -metadata copyright="power-prods" "!all_audio_file!" -y 2>&1
93
+
94
+ if !ERRORLEVEL! EQU 0 (
95
+ echo [OK] Archivo con todos los audios creado: !folder_name!_all_audio.mp4
96
+ ) else (
97
+ echo [ERROR] Fallo al crear el archivo con todos los audios.
98
+ )
99
+
100
+ echo.
101
+
102
+ :: PASO 5: Convertir solo el audio elegido + video copy
103
+ set "selected_audio_file=!output_dir!\!folder_name!_selected_audio.mp4"
104
+ echo Convirtiendo audio seleccionado a FLAC con video copy...
105
+
106
+ ffmpeg -i "%input_file%" -map 0:v -c:v copy -map 0:a:!selected_audio_index! -c:a flac -compression_level 0 -metadata title="power-prods" -metadata comment="power-prods" -metadata copyright="power-prods" "!selected_audio_file!" -y 2>&1
107
+
108
+ if !ERRORLEVEL! EQU 0 (
109
+ echo [OK] Archivo con audio seleccionado creado: !folder_name!_selected_audio.mp4
110
+ ) else (
111
+ echo [ERROR] Fallo al crear el archivo con audio seleccionado.
112
+ )
113
+
114
+ echo.
115
+
116
+ :: PASO 6: Extraer subt�tulos y convertir a VTT
117
+ echo Extrayendo subtitulos...
118
+
119
+ set subtitle_count=0
120
+ for /f "tokens=1" %%s in ('ffprobe -v error -select_streams s -show_entries stream^=index -of csv^=p^=0 "%input_file%" 2^>^&1') do (
121
+ set /a subtitle_count+=1
122
+ set "sub_index=%%s"
123
+
124
+ :: Obtener informaci�n del subt�tulo
125
+ for /f "delims=" %%l in ('ffprobe -v error -select_streams s:!subtitle_count! -show_entries stream_tags^=language -of default^=noprint_wrappers^=1:nokey^=1 "%input_file%" 2^>^&1') do set "sub_lang=%%l"
126
+
127
+ if "!sub_lang!"=="" set "sub_lang=und"
128
+
129
+ set "subtitle_file=!output_dir!\!folder_name!_sub_!subtitle_count!_!sub_lang!.vtt"
130
+
131
+ ffmpeg -i "%input_file%" -map 0:s:!subtitle_count! "!subtitle_file!" -y 2>&1
132
+
133
+ if !ERRORLEVEL! EQU 0 (
134
+ echo [OK] Subtitulo !subtitle_count! extraido: !folder_name!_sub_!subtitle_count!_!sub_lang!.vtt
135
+ )
136
+ )
137
+
138
+ if !subtitle_count! EQU 0 (
139
+ echo No se encontraron subtitulos en el video.
140
+ )
141
+
142
+ echo.
143
+ echo ============================================
144
+ echo Procesamiento completado para "%video_name%"
145
+ echo Archivos guardados en: !folder_name!
146
+ echo ============================================
147
+ echo.
148
+
149
+ :: Preguntar si eliminar el archivo original
150
+ set /p "delete_original=Deseas eliminar el archivo original? (S/N): "
151
+ if /i "!delete_original!"=="S" (
152
+ del "%input_file%"
153
+ echo Archivo original eliminado.
154
+ ) else (
155
+ echo Archivo original conservado.
156
+ )
157
+
158
+ echo.
159
+ shift
160
+ goto loop
161
+
162
+ :endloop
163
+ echo.
164
+ echo ============================================
165
+ echo Todos los archivos han sido procesados.
166
+ echo ============================================
167
+ pause
168
+ endlocal
job.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3e4b012b547d5da7a93cd6c615576d3f18dae741d3066c7c97958c82f9e9e50f
3
+ size 11493654
job.py ADDED
@@ -0,0 +1,836 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import subprocess
4
+ import threading
5
+ import re
6
+ import json
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Optional, List
10
+ import tkinter as tk
11
+ from tkinter import filedialog, messagebox, scrolledtext
12
+
13
+ # ============================================================================
14
+ # TEMA
15
+ # ============================================================================
16
+ class Theme:
17
+ BG = "#0f0f1a"
18
+ BG_CARD = "#1a1a2e"
19
+ BG_INPUT = "#252542"
20
+ BG_HOVER = "#2d2d4a"
21
+ ACCENT = "#6366f1"
22
+ ACCENT_HOVER = "#818cf8"
23
+ DANGER = "#ef4444"
24
+ SUCCESS = "#22c55e"
25
+ TEXT = "#f8fafc"
26
+ TEXT_DIM = "#94a3b8"
27
+
28
+ # ============================================================================
29
+ # CLASES DE DATOS
30
+ # ============================================================================
31
+ class AudioTrack:
32
+ def __init__(self, index, stream_index, codec, language, channels, title=""):
33
+ self.index = index
34
+ self.stream_index = stream_index
35
+ self.codec = codec
36
+ self.language = language
37
+ self.channels = channels
38
+ self.title = title
39
+
40
+ def display_name(self):
41
+ lang = self.language if self.language != "und" else "Unknown"
42
+ ch = f"{self.channels}ch" if self.channels else ""
43
+ title_part = f" - {self.title}" if self.title else ""
44
+ return f"[{self.index}] {lang} | {self.codec} {ch}{title_part}"
45
+
46
+ class SubtitleTrack:
47
+ def __init__(self, index, stream_index, codec, language, title="", forced=False):
48
+ self.index = index
49
+ self.stream_index = stream_index
50
+ self.codec = codec
51
+ self.language = language
52
+ self.title = title
53
+ self.forced = forced
54
+
55
+ def display_name(self):
56
+ lang = self.language if self.language != "und" else "Unknown"
57
+ forced_tag = " (Forced)" if self.forced else ""
58
+ title_part = f" - {self.title}" if self.title else ""
59
+ return f"[{self.index}] {lang} | {self.codec}{forced_tag}{title_part}"
60
+
61
+ class MediaInfo:
62
+ def __init__(self, path):
63
+ self.path = path
64
+ self.audio_tracks: List[AudioTrack] = []
65
+ self.subtitle_tracks: List[SubtitleTrack] = []
66
+ self.duration = 0.0
67
+ self.title: Optional[str] = None
68
+
69
+ # ============================================================================
70
+ # MOTOR DE CONVERSIÓN
71
+ # ============================================================================
72
+ class MediaAnalyzer:
73
+ @staticmethod
74
+ def get_media_info(source: str) -> MediaInfo:
75
+ info = MediaInfo(path=source)
76
+
77
+ try:
78
+ cmd = ["ffprobe", "-v", "quiet", "-print_format", "json",
79
+ "-show_format", "-show_streams", source]
80
+
81
+ creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
82
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, creationflags=creationflags)
83
+
84
+ if result.returncode != 0:
85
+ return info
86
+
87
+ data = json.loads(result.stdout)
88
+
89
+ format_tags = data.get("format", {}).get("tags", {})
90
+ info.title = format_tags.get("title") or format_tags.get("TITLE")
91
+ if info.title:
92
+ info.title = info.title.strip()
93
+
94
+ if "format" in data and "duration" in data["format"]:
95
+ info.duration = float(data["format"]["duration"])
96
+
97
+ audio_idx = 0
98
+ sub_idx = 0
99
+
100
+ for stream in data.get("streams", []):
101
+ codec_type = stream.get("codec_type", "")
102
+ tags = stream.get("tags", {})
103
+
104
+ if codec_type == "audio":
105
+ track = AudioTrack(
106
+ index=audio_idx,
107
+ stream_index=stream.get("index", 0),
108
+ codec=stream.get("codec_name", "unknown"),
109
+ language=tags.get("language", "und"),
110
+ channels=stream.get("channels", 2),
111
+ title=tags.get("title", "")
112
+ )
113
+ info.audio_tracks.append(track)
114
+ audio_idx += 1
115
+
116
+ elif codec_type == "subtitle":
117
+ disposition = stream.get("disposition", {})
118
+ track = SubtitleTrack(
119
+ index=sub_idx,
120
+ stream_index=stream.get("index", 0),
121
+ codec=stream.get("codec_name", "unknown"),
122
+ language=tags.get("language", "und"),
123
+ title=tags.get("title", ""),
124
+ forced=disposition.get("forced", 0) == 1
125
+ )
126
+ info.subtitle_tracks.append(track)
127
+ sub_idx += 1
128
+ except Exception as e:
129
+ print(f"Error analyzing: {e}")
130
+
131
+ return info
132
+
133
+ @staticmethod
134
+ def extract_subtitle_preview(source: str, subtitle_index: int) -> str:
135
+ try:
136
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.vtt', delete=False) as tmp:
137
+ tmp_path = tmp.name
138
+
139
+ cmd = ["ffmpeg", "-y", "-i", source, "-map", f"0:s:{subtitle_index}", "-t", "300", tmp_path]
140
+ creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
141
+ subprocess.run(cmd, capture_output=True, timeout=30, creationflags=creationflags)
142
+
143
+ if os.path.exists(tmp_path):
144
+ with open(tmp_path, 'r', encoding='utf-8', errors='ignore') as f:
145
+ content = f.read()
146
+ os.unlink(tmp_path)
147
+ lines = [l.strip() for l in content.split('\n') if l.strip() and not l.strip().startswith('WEBVTT') and '-->' not in l]
148
+ return '\n'.join(lines[:30])
149
+ except Exception as e:
150
+ return f"Error: {e}"
151
+ return "No se pudo cargar"
152
+
153
+ class VideoConverter:
154
+ def __init__(self):
155
+ self.current_process = None
156
+ self.cancelled = False
157
+
158
+ def cancel(self):
159
+ self.cancelled = True
160
+ if self.current_process:
161
+ try:
162
+ self.current_process.terminate()
163
+ except:
164
+ pass
165
+
166
+ def run_ffmpeg(self, cmd: list, duration: float, progress_cb, status_cb, msg: str) -> bool:
167
+ try:
168
+ creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
169
+ self.current_process = subprocess.Popen(
170
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
171
+ universal_newlines=True, creationflags=creationflags
172
+ )
173
+
174
+ pattern = re.compile(r"time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})")
175
+
176
+ for line in self.current_process.stderr:
177
+ if self.cancelled:
178
+ self.current_process.terminate()
179
+ return False
180
+
181
+ match = pattern.search(line)
182
+ if match and duration > 0:
183
+ h, m, s, ms = map(int, match.groups())
184
+ current = h * 3600 + m * 60 + s + ms / 100
185
+ progress = min(100, (current / duration) * 100)
186
+ if progress_cb:
187
+ progress_cb(progress)
188
+ if status_cb:
189
+ status_cb(f"{msg}: {progress:.0f}%")
190
+
191
+ self.current_process.wait()
192
+ return self.current_process.returncode == 0
193
+ except Exception as e:
194
+ print(f"FFmpeg error: {e}")
195
+ return False
196
+
197
+ def convert(self, source: str, is_url: bool, mode: str, output_dir: str,
198
+ selected_audio: int, generate_single: bool, extract_sub: bool, sub_index: int,
199
+ progress_cb, status_cb) -> bool:
200
+ self.cancelled = False
201
+
202
+ try:
203
+ # Headers para URLs directas (mejora compatibilidad)
204
+ extra_input_options = []
205
+ if is_url:
206
+ extra_input_options = [
207
+ "-user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
208
+ "-headers", "Referer: https://google.com\r\n"
209
+ ]
210
+
211
+ info = MediaAnalyzer.get_media_info(source)
212
+ duration = info.duration if info.duration > 0 else 1
213
+
214
+ base_name = info.title if info.title else (Path(source).stem if not is_url else "video_directo")
215
+ if not base_name or base_name.strip() == "":
216
+ base_name = "video_sin_nombre"
217
+
218
+ folder_name = re.sub(r'[<>:"/\\|?*]', '_', base_name)
219
+ folder_name = re.sub(r'\s+', ' ', folder_name).strip()
220
+ folder_name = folder_name.replace(' ', '_')
221
+ for old, new in [("a", "4"), ("A", "4"), ("i", "1"), ("I", "1"), ("o", "0"), ("O", "0")]:
222
+ folder_name = folder_name.replace(old, new)
223
+ folder_name = folder_name[:150]
224
+ if not folder_name:
225
+ folder_name = "video_sin_nombre"
226
+
227
+ out_folder = os.path.join(output_dir, folder_name)
228
+ os.makedirs(out_folder, exist_ok=True)
229
+
230
+ if status_cb:
231
+ status_cb(f"Carpeta creada: {out_folder}")
232
+
233
+ # PASO 1: Archivo principal con todos los audios
234
+ if status_cb:
235
+ status_cb(f"[1/{'3' if generate_single else '2'}] Convirtiendo todos los audios: {base_name}")
236
+
237
+ all_audio_path = os.path.join(out_folder, f"{folder_name}_all_audio.mp4")
238
+
239
+ cmd = ["ffmpeg", "-y"] + extra_input_options + ["-i", source, "-map", "0:v:0", "-map", "0:a?"]
240
+
241
+ if mode == "Video Copy + Audio MP3":
242
+ cmd.extend(["-c:v", "copy", "-c:a", "libmp3lame", "-b:a", "320k"])
243
+ elif mode == "Video Copy + Audio FLAC":
244
+ cmd.extend(["-c:v", "copy", "-c:a", "flac", "-compression_level", "0"])
245
+ else:
246
+ cmd.extend(["-c:v", "libx264", "-vf", "scale=-2:1080", "-preset", "slow",
247
+ "-crf", "18", "-c:a", "libmp3lame", "-b:a", "320k"])
248
+
249
+ cmd.extend([
250
+ "-map_metadata", "0",
251
+ "-metadata", "copyright=Power Prods",
252
+ "-metadata", "description=Power Prods",
253
+ all_audio_path
254
+ ])
255
+
256
+ if progress_cb:
257
+ progress_cb(0)
258
+
259
+ success_all = self.run_ffmpeg(cmd, duration, progress_cb, status_cb, f"[1/{'3' if generate_single else '2'}] Todos los audios")
260
+
261
+ if not success_all or self.cancelled:
262
+ if status_cb:
263
+ status_cb("Error en conversión principal")
264
+ return False
265
+
266
+ # PASO 2: Extraer subtítulo
267
+ if extract_sub:
268
+ if status_cb:
269
+ status_cb(f"[2/{'3' if generate_single else '2'}] Extrayendo subtítulo")
270
+
271
+ vtt_path = os.path.join(out_folder, f"{folder_name}_sub_{sub_index}.vtt")
272
+ sub_cmd = ["ffmpeg", "-y"] + extra_input_options + ["-i", source, "-map", f"0:s:{sub_index}", "-c:s", "webvtt", vtt_path]
273
+ creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
274
+ subprocess.run(sub_cmd, capture_output=True, creationflags=creationflags)
275
+
276
+ # PASO OPCIONAL: Versión adicional
277
+ if generate_single:
278
+ if status_cb:
279
+ status_cb(f"[{'3' if extract_sub else '2'}/3] Generando versión audio seleccionado")
280
+
281
+ single_path = os.path.join(out_folder, f"{folder_name}_audio_{selected_audio}.mp4")
282
+
283
+ single_cmd = [
284
+ "ffmpeg", "-y", "-i", all_audio_path,
285
+ "-map", "0:v:0", "-map", f"0:a:{selected_audio}",
286
+ "-c:v", "copy", "-c:a", "copy",
287
+ "-map_metadata", "0",
288
+ "-metadata", "copyright=Power Prods",
289
+ "-metadata", "description=Power Prods",
290
+ single_path
291
+ ]
292
+
293
+ success_single = self.run_ffmpeg(single_cmd, duration, progress_cb, status_cb, "[3/3] Audio seleccionado")
294
+
295
+ if not success_single or self.cancelled:
296
+ return False
297
+
298
+ # FINAL
299
+ if not self.cancelled:
300
+ if not is_url:
301
+ try:
302
+ os.remove(source)
303
+ if status_cb:
304
+ status_cb("Original local borrado")
305
+ except Exception as e:
306
+ if status_cb:
307
+ status_cb(f"No borrado original: {e}")
308
+
309
+ if progress_cb:
310
+ progress_cb(100)
311
+ files_created = f"{folder_name}_all_audio.mp4"
312
+ if generate_single:
313
+ files_created += f" + {folder_name}_audio_{selected_audio}.mp4"
314
+ if status_cb:
315
+ status_cb(f"¡COMPLETADO! Carpeta: {out_folder}")
316
+ status_cb(f"Archivos: {files_created}")
317
+ return True
318
+
319
+ except Exception as e:
320
+ if status_cb:
321
+ status_cb(f"Error crítico: {e}")
322
+ return False
323
+
324
+ # ============================================================================
325
+ # COMPONENTES UI
326
+ # ============================================================================
327
+ class AnimatedProgress(tk.Canvas):
328
+ def __init__(self, parent, **kwargs):
329
+ super().__init__(parent, height=14, bg=Theme.BG_INPUT, highlightthickness=0, **kwargs)
330
+ self.progress = 0
331
+ self.pulse_offset = 0
332
+ self.animating = False
333
+ self.bind("<Configure>", lambda e: self.draw())
334
+
335
+ def set_progress(self, value: float):
336
+ self.progress = max(0, min(100, value))
337
+ self.draw()
338
+
339
+ def draw(self):
340
+ self.delete("all")
341
+ w = self.winfo_width() or 400
342
+ h = self.winfo_height() or 14
343
+
344
+ self.create_rectangle(0, 0, w, h, fill=Theme.BG_INPUT, outline="")
345
+
346
+ if self.progress > 0:
347
+ pw = (self.progress / 100) * w
348
+ self.create_rectangle(0, 0, pw, h, fill=Theme.ACCENT, outline="")
349
+
350
+ if self.animating and pw > 0:
351
+ for i in range(3):
352
+ px = ((self.pulse_offset + i * 40) % int(pw + 80)) - 40
353
+ if 0 < px < pw:
354
+ self.create_rectangle(px, 0, min(px + 40, pw), h, fill=Theme.ACCENT_HOVER, outline="")
355
+
356
+ def start_pulse(self):
357
+ self.animating = True
358
+ self._pulse()
359
+
360
+ def stop_pulse(self):
361
+ self.animating = False
362
+
363
+ def _pulse(self):
364
+ if self.animating:
365
+ self.pulse_offset += 4
366
+ self.draw()
367
+ self.after(50, self._pulse)
368
+
369
+ # ============================================================================
370
+ # APLICACIÓN PRINCIPAL
371
+ # ============================================================================
372
+ class VideoConverterApp:
373
+ def __init__(self):
374
+ self.root = tk.Tk()
375
+ self.root.title("Video Converter Pro")
376
+ self.root.geometry("600x900")
377
+ self.root.minsize(500, 700)
378
+ self.root.config(bg=Theme.BG)
379
+
380
+ self.root.update_idletasks()
381
+ x = (self.root.winfo_screenwidth() - 600) // 2
382
+ y = (self.root.winfo_screenheight() - 900) // 2
383
+ self.root.geometry(f"+{x}+{y}")
384
+
385
+ self.converter = VideoConverter()
386
+ self.is_converting = False
387
+ self.mode_var = tk.StringVar(value="Video Copy + Audio MP3")
388
+ self.extract_subs_var = tk.BooleanVar(value=False)
389
+ self.generate_single_var = tk.BooleanVar(value=False)
390
+
391
+ # Carpeta de salida predeterminada: donde está el .py
392
+ default_output = os.path.dirname(os.path.abspath(__file__))
393
+ self.output_dir_var = tk.StringVar(value=default_output)
394
+
395
+ self.files: List[str] = []
396
+ self.urls: List[str] = []
397
+ self.current_media_info: Optional[MediaInfo] = None
398
+
399
+ self._build_ui()
400
+
401
+ def _build_ui(self):
402
+ container = tk.Frame(self.root, bg=Theme.BG)
403
+ container.pack(fill="both", expand=True)
404
+
405
+ canvas = tk.Canvas(container, bg=Theme.BG, highlightthickness=0)
406
+ scrollbar = tk.Scrollbar(container, orient="vertical", command=canvas.yview)
407
+ self.main = tk.Frame(canvas, bg=Theme.BG)
408
+
409
+ canvas.configure(yscrollcommand=scrollbar.set)
410
+ scrollbar.pack(side="right", fill="y")
411
+ canvas.pack(side="left", fill="both", expand=True)
412
+
413
+ canvas_window = canvas.create_window((0, 0), window=self.main, anchor="nw")
414
+
415
+ def on_configure(e):
416
+ canvas.configure(scrollregion=canvas.bbox("all"))
417
+ canvas.itemconfig(canvas_window, width=e.width)
418
+
419
+ self.main.bind("<Configure>", on_configure)
420
+ canvas.bind("<Configure>", lambda e: canvas.itemconfig(canvas_window, width=e.width))
421
+ canvas.bind_all("<MouseWheel>", lambda e: canvas.yview_scroll(int(-1*(e.delta/120)), "units"))
422
+
423
+ header = tk.Frame(self.main, bg=Theme.BG)
424
+ header.pack(fill="x", padx=20, pady=(20, 16))
425
+
426
+ tk.Label(header, text="Video Converter Pro", font=("Segoe UI", 20, "bold"),
427
+ bg=Theme.BG, fg=Theme.TEXT).pack(anchor="w")
428
+ tk.Label(header, text="Convierte videos locales o desde links directos",
429
+ font=("Segoe UI", 10), bg=Theme.BG, fg=Theme.TEXT_DIM).pack(anchor="w")
430
+
431
+ # SECCIÓN CARPETA DE SALIDA
432
+ self._label("Carpeta de Salida")
433
+ output_card = tk.Frame(self.main, bg=Theme.BG_CARD)
434
+ output_card.pack(fill="x", padx=20, pady=(0, 10))
435
+
436
+ tk.Label(output_card, text="Selecciona donde guardar los videos convertidos (predeterminado: carpeta del programa):",
437
+ font=("Segoe UI", 10), bg=Theme.BG_CARD, fg=Theme.TEXT).pack(anchor="w", padx=10, pady=(10, 5))
438
+
439
+ dir_frame = tk.Frame(output_card, bg=Theme.BG_CARD)
440
+ dir_frame.pack(fill="x", padx=10, pady=(0, 10))
441
+
442
+ self.output_entry = tk.Entry(dir_frame, textvariable=self.output_dir_var, font=("Segoe UI", 9),
443
+ bg=Theme.BG_INPUT, fg=Theme.TEXT, relief="flat")
444
+ self.output_entry.pack(side="left", fill="x", expand=True, ipady=6)
445
+
446
+ tk.Button(dir_frame, text="Cambiar carpeta", font=("Segoe UI", 10, "bold"),
447
+ bg=Theme.ACCENT, fg=Theme.TEXT, relief="flat", bd=0, cursor="hand2",
448
+ command=self._choose_output_dir).pack(side="right", ipadx=10, ipady=6)
449
+
450
+ self._label("Archivos Locales")
451
+
452
+ files_card = tk.Frame(self.main, bg=Theme.BG_CARD)
453
+ files_card.pack(fill="x", padx=20, pady=(0, 10))
454
+
455
+ self.files_list = tk.Listbox(files_card, font=("Segoe UI", 9), bg=Theme.BG_INPUT, fg=Theme.TEXT,
456
+ selectbackground=Theme.ACCENT, height=4, relief="flat", bd=0,
457
+ highlightthickness=0, exportselection=False)
458
+ self.files_list.pack(fill="x", padx=10, pady=10)
459
+ self.files_list.bind("<<ListboxSelect>>", self._on_file_select)
460
+ self.files_list.bind("<Double-Button-1>", self._remove_file)
461
+
462
+ btn_row = tk.Frame(files_card, bg=Theme.BG_CARD)
463
+ btn_row.pack(fill="x", padx=10, pady=(0, 10))
464
+
465
+ tk.Button(btn_row, text="Seleccionar archivos", font=("Segoe UI", 10, "bold"),
466
+ bg=Theme.ACCENT, fg=Theme.TEXT, activebackground=Theme.ACCENT_HOVER,
467
+ activeforeground=Theme.TEXT, relief="flat", bd=0, cursor="hand2",
468
+ command=self._browse_files).pack(side="left", fill="x", expand=True, ipady=8, padx=(0, 4))
469
+
470
+ tk.Button(btn_row, text="Limpiar", font=("Segoe UI", 10),
471
+ bg=Theme.BG_INPUT, fg=Theme.TEXT, activebackground=Theme.BG_HOVER,
472
+ relief="flat", bd=0, cursor="hand2",
473
+ command=self._clear_files).pack(side="left", fill="x", expand=True, ipady=8, padx=(4, 0))
474
+
475
+ self.files_count = tk.Label(files_card, text="0 archivos", font=("Segoe UI", 9),
476
+ bg=Theme.BG_CARD, fg=Theme.TEXT_DIM)
477
+ self.files_count.pack(anchor="e", padx=10, pady=(0, 6))
478
+
479
+ self._label("Pistas de Audio (del archivo seleccionado)")
480
+
481
+ audio_card = tk.Frame(self.main, bg=Theme.BG_CARD)
482
+ audio_card.pack(fill="x", padx=20, pady=(0, 10))
483
+
484
+ self.audio_list = tk.Listbox(audio_card, font=("Segoe UI", 9), bg=Theme.BG_INPUT, fg=Theme.TEXT,
485
+ selectbackground=Theme.ACCENT, height=3, relief="flat", bd=0,
486
+ highlightthickness=0, exportselection=False)
487
+ self.audio_list.pack(fill="x", padx=10, pady=10)
488
+
489
+ self.audio_info = tk.Label(audio_card, text="Selecciona un archivo para ver sus pistas",
490
+ font=("Segoe UI", 9), bg=Theme.BG_CARD, fg=Theme.TEXT_DIM)
491
+ self.audio_info.pack(anchor="w", padx=10, pady=(0, 4))
492
+
493
+ single_frame = tk.Frame(audio_card, bg=Theme.BG_CARD)
494
+ single_frame.pack(fill="x", padx=10, pady=(0, 10))
495
+ tk.Checkbutton(single_frame, text="Generar versión ADICIONAL con solo el audio seleccionado",
496
+ variable=self.generate_single_var, font=("Segoe UI", 10),
497
+ bg=Theme.BG_CARD, fg=Theme.TEXT, selectcolor=Theme.BG_INPUT,
498
+ activebackground=Theme.BG_CARD).pack(anchor="w")
499
+
500
+ self._label("Subtitulos")
501
+
502
+ sub_card = tk.Frame(self.main, bg=Theme.BG_CARD)
503
+ sub_card.pack(fill="x", padx=20, pady=(0, 10))
504
+
505
+ check_frame = tk.Frame(sub_card, bg=Theme.BG_CARD)
506
+ check_frame.pack(fill="x", padx=10, pady=(10, 4))
507
+
508
+ tk.Checkbutton(check_frame, text="Extraer subtitulo seleccionado a VTT",
509
+ variable=self.extract_subs_var, font=("Segoe UI", 10),
510
+ bg=Theme.BG_CARD, fg=Theme.TEXT, selectcolor=Theme.BG_INPUT,
511
+ activebackground=Theme.BG_CARD).pack(anchor="w")
512
+
513
+ self.sub_list = tk.Listbox(sub_card, font=("Segoe UI", 9), bg=Theme.BG_INPUT, fg=Theme.TEXT,
514
+ selectbackground=Theme.ACCENT, height=3, relief="flat", bd=0,
515
+ highlightthickness=0, exportselection=False)
516
+ self.sub_list.pack(fill="x", padx=10, pady=4)
517
+
518
+ tk.Button(sub_card, text="Preview subtitulo", font=("Segoe UI", 10),
519
+ bg=Theme.BG_INPUT, fg=Theme.TEXT, activebackground=Theme.BG_HOVER,
520
+ relief="flat", bd=0, cursor="hand2",
521
+ command=self._preview_subtitle).pack(fill="x", padx=10, pady=(4, 10), ipady=6)
522
+
523
+ self._label("Links Directos (URLs de video)")
524
+
525
+ url_card = tk.Frame(self.main, bg=Theme.BG_CARD)
526
+ url_card.pack(fill="x", padx=20, pady=(0, 10))
527
+
528
+ url_input = tk.Frame(url_card, bg=Theme.BG_CARD)
529
+ url_input.pack(fill="x", padx=10, pady=10)
530
+
531
+ self.url_entry = tk.Entry(url_input, font=("Segoe UI", 10), bg=Theme.BG_INPUT, fg=Theme.TEXT,
532
+ insertbackground=Theme.TEXT, relief="flat", bd=0)
533
+ self.url_entry.pack(side="left", fill="x", expand=True, ipady=10, padx=(0, 8))
534
+ self.url_entry.insert(0, "Pegar link directo de video (.mp4, .mkv, etc.)")
535
+ self.url_entry.bind("<FocusIn>", lambda e: self.url_entry.delete(0, tk.END) if "Pegar link" in self.url_entry.get() else None)
536
+ self.url_entry.bind("<Return>", lambda e: self._add_url())
537
+
538
+ tk.Button(url_input, text="+ Agregar", font=("Segoe UI", 10, "bold"),
539
+ bg=Theme.ACCENT, fg=Theme.TEXT, relief="flat", bd=0, cursor="hand2",
540
+ command=self._add_url).pack(side="right", ipady=6, ipadx=12)
541
+
542
+ self.url_list = tk.Listbox(url_card, font=("Segoe UI", 9), bg=Theme.BG_INPUT, fg=Theme.TEXT,
543
+ selectbackground=Theme.ACCENT, height=3, relief="flat", bd=0,
544
+ highlightthickness=0)
545
+ self.url_list.pack(fill="x", padx=10, pady=(0, 4))
546
+ self.url_list.bind("<Double-Button-1>", self._remove_url)
547
+
548
+ self.urls_count = tk.Label(url_card, text="0 URLs", font=("Segoe UI", 9),
549
+ bg=Theme.BG_CARD, fg=Theme.TEXT_DIM)
550
+ self.urls_count.pack(anchor="e", padx=10, pady=(0, 8))
551
+
552
+ self._label("Modo de Conversion")
553
+
554
+ mode_card = tk.Frame(self.main, bg=Theme.BG_CARD)
555
+ mode_card.pack(fill="x", padx=20, pady=(0, 10))
556
+
557
+ modes = ["Video Copy + Audio MP3", "Video Copy + Audio FLAC", "Video H264 1080p + Audio MP3"]
558
+ for mode in modes:
559
+ tk.Radiobutton(mode_card, text=mode, variable=self.mode_var, value=mode,
560
+ font=("Segoe UI", 10), bg=Theme.BG_CARD, fg=Theme.TEXT,
561
+ selectcolor=Theme.BG_INPUT, activebackground=Theme.BG_CARD,
562
+ highlightthickness=0, cursor="hand2").pack(anchor="w", padx=14, pady=4)
563
+
564
+ self._label("Progreso")
565
+
566
+ progress_card = tk.Frame(self.main, bg=Theme.BG_CARD)
567
+ progress_card.pack(fill="x", padx=20, pady=(0, 10))
568
+
569
+ self.progress_label = tk.Label(progress_card, text="0%", font=("Segoe UI", 24, "bold"),
570
+ bg=Theme.BG_CARD, fg=Theme.ACCENT)
571
+ self.progress_label.pack(anchor="w", padx=14, pady=(10, 4))
572
+
573
+ self.progress_bar = AnimatedProgress(progress_card)
574
+ self.progress_bar.pack(fill="x", padx=14, pady=4)
575
+
576
+ self.status_label = tk.Label(progress_card, text="Listo para convertir", font=("Segoe UI", 10),
577
+ bg=Theme.BG_CARD, fg=Theme.TEXT_DIM, anchor="w")
578
+ self.status_label.pack(fill="x", padx=14, pady=4)
579
+
580
+ self.log_text = tk.Text(progress_card, font=("Consolas", 9), bg=Theme.BG_INPUT, fg=Theme.TEXT_DIM,
581
+ height=8, relief="flat", bd=0, state="disabled", wrap="word", padx=10, pady=8)
582
+ self.log_text.pack(fill="x", padx=14, pady=(4, 14))
583
+
584
+ action_frame = tk.Frame(self.main, bg=Theme.BG)
585
+ action_frame.pack(fill="x", padx=20, pady=(6, 20))
586
+
587
+ self.btn_start = tk.Button(action_frame, text="INICIAR CONVERSION", font=("Segoe UI", 12, "bold"),
588
+ bg=Theme.ACCENT, fg=Theme.TEXT, activebackground=Theme.ACCENT_HOVER,
589
+ activeforeground=Theme.TEXT, relief="flat", bd=0, cursor="hand2",
590
+ command=self._start_conversion)
591
+ self.btn_start.pack(fill="x", ipady=14, pady=(0, 8))
592
+
593
+ self.btn_cancel = tk.Button(action_frame, text="CANCELAR", font=("Segoe UI", 11, "bold"),
594
+ bg=Theme.DANGER, fg=Theme.TEXT, relief="flat", bd=0, cursor="hand2",
595
+ state="disabled", command=self._cancel_conversion)
596
+ self.btn_cancel.pack(fill="x", ipady=10)
597
+
598
+ def _label(self, text):
599
+ frame = tk.Frame(self.main, bg=Theme.BG)
600
+ frame.pack(fill="x", padx=20, pady=(12, 6))
601
+ tk.Label(frame, text=text, font=("Segoe UI", 11, "bold"),
602
+ bg=Theme.BG, fg=Theme.ACCENT).pack(anchor="w")
603
+
604
+ def _choose_output_dir(self):
605
+ dir_path = filedialog.askdirectory(title="Seleccionar carpeta donde guardar los videos convertidos")
606
+ if dir_path:
607
+ self.output_dir_var.set(dir_path)
608
+ self._log(f"Carpeta de salida cambiada a: {dir_path}")
609
+
610
+ def _browse_files(self):
611
+ files = filedialog.askopenfilenames(
612
+ title="Seleccionar videos",
613
+ filetypes=[("Videos", "*.mp4 *.mkv *.avi *.mov *.webm *.ts"), ("Todos", "*.*")]
614
+ )
615
+ if files:
616
+ for f in files:
617
+ if f not in self.files:
618
+ self.files.append(f)
619
+ self._update_files_ui()
620
+
621
+ def _update_files_ui(self):
622
+ self.files_list.delete(0, tk.END)
623
+ for f in self.files:
624
+ self.files_list.insert(tk.END, f" {os.path.basename(f)}")
625
+
626
+ self.files_count.config(text=f"{len(self.files)} archivo{'s' if len(self.files) != 1 else ''}")
627
+
628
+ if self.files:
629
+ self.files_list.selection_set(0)
630
+ self._analyze_file(self.files[0])
631
+
632
+ def _on_file_select(self, e):
633
+ sel = self.files_list.curselection()
634
+ if sel and self.files:
635
+ self._analyze_file(self.files[sel[0]])
636
+
637
+ def _analyze_file(self, filepath: str):
638
+ self.status_label.config(text=f"Analizando: {os.path.basename(filepath)}...")
639
+ self.root.update_idletasks()
640
+
641
+ def analyze():
642
+ info = MediaAnalyzer.get_media_info(filepath)
643
+ self.root.after(0, lambda: self._show_tracks(info))
644
+
645
+ threading.Thread(target=analyze, daemon=True).start()
646
+
647
+ def _show_tracks(self, info: MediaInfo):
648
+ self.current_media_info = info
649
+
650
+ self.audio_list.delete(0, tk.END)
651
+ for track in info.audio_tracks:
652
+ self.audio_list.insert(tk.END, f" {track.display_name()}")
653
+ if info.audio_tracks:
654
+ self.audio_info.config(text=f"{len(info.audio_tracks)} pista(s) de audio disponible(s)")
655
+ else:
656
+ self.audio_info.config(text="Sin pistas de audio")
657
+
658
+ self.sub_list.delete(0, tk.END)
659
+ for track in info.subtitle_tracks:
660
+ self.sub_list.insert(tk.END, f" {track.display_name()}")
661
+ if info.subtitle_tracks:
662
+ self.sub_list.selection_set(0)
663
+
664
+ self.status_label.config(text="Listo para convertir")
665
+
666
+ def _preview_subtitle(self):
667
+ if not self.current_media_info or not self.current_media_info.subtitle_tracks:
668
+ messagebox.showinfo("Info", "No hay subtítulos")
669
+ return
670
+
671
+ sel = self.sub_list.curselection()
672
+ idx = sel[0] if sel else 0
673
+
674
+ self.status_label.config(text="Cargando preview...")
675
+ self.root.update_idletasks()
676
+
677
+ def load():
678
+ content = MediaAnalyzer.extract_subtitle_preview(self.current_media_info.path, idx)
679
+ self.root.after(0, lambda: self._show_preview(content))
680
+
681
+ threading.Thread(target=load, daemon=True).start()
682
+
683
+ def _show_preview(self, content):
684
+ self.status_label.config(text="Listo")
685
+
686
+ win = tk.Toplevel(self.root)
687
+ win.title("Preview Subtitulo")
688
+ win.geometry("500x400")
689
+ win.config(bg=Theme.BG)
690
+
691
+ text = scrolledtext.ScrolledText(win, font=("Consolas", 10), bg=Theme.BG_INPUT, fg=Theme.TEXT)
692
+ text.pack(fill="both", expand=True, padx=16, pady=16)
693
+ text.insert("1.0", content)
694
+ text.config(state="disabled")
695
+
696
+ def _remove_file(self, e):
697
+ sel = self.files_list.curselection()
698
+ if sel:
699
+ del self.files[sel[0]]
700
+ self._update_files_ui()
701
+
702
+ def _clear_files(self):
703
+ self.files = []
704
+ self._update_files_ui()
705
+ self.audio_list.delete(0, tk.END)
706
+ self.sub_list.delete(0, tk.END)
707
+ self.audio_info.config(text="Selecciona un archivo para ver sus pistas")
708
+ self.current_media_info = None
709
+
710
+ def _add_url(self):
711
+ url = self.url_entry.get().strip()
712
+ if url and url.startswith("http"):
713
+ if url not in self.urls:
714
+ self.urls.append(url)
715
+ display = url if len(url) <= 60 else url[:57] + "..."
716
+ self.url_list.insert(tk.END, f" {display}")
717
+ self.urls_count.config(text=f"{len(self.urls)} URL{'s' if len(self.urls) != 1 else ''}")
718
+ self.url_entry.delete(0, tk.END)
719
+
720
+ def _remove_url(self, e):
721
+ sel = self.url_list.curselection()
722
+ if sel:
723
+ del self.urls[sel[0]]
724
+ self.url_list.delete(sel[0])
725
+ self.urls_count.config(text=f"{len(self.urls)} URL{'s' if len(self.urls) != 1 else ''}")
726
+
727
+ def _log(self, msg):
728
+ self.log_text.config(state="normal")
729
+ self.log_text.insert(tk.END, f"{msg}\n")
730
+ self.log_text.see(tk.END)
731
+ self.log_text.config(state="disabled")
732
+
733
+ def _update_progress(self, value):
734
+ self.progress_bar.set_progress(value)
735
+ self.progress_label.config(text=f"{int(value)}%")
736
+
737
+ def _update_status(self, text):
738
+ self.status_label.config(text=text)
739
+ self._log(text)
740
+
741
+ def _start_conversion(self):
742
+ if not self.files and not self.urls:
743
+ messagebox.showwarning("Nada que convertir", "Agrega archivos locales o links directos")
744
+ return
745
+
746
+ output_dir = self.output_dir_var.get()
747
+ if not os.path.isdir(output_dir):
748
+ messagebox.showerror("Carpeta inválida", "La carpeta de salida no existe o no es válida")
749
+ return
750
+
751
+ self._log(f"Carpeta de salida seleccionada: {output_dir}")
752
+
753
+ mode = self.mode_var.get()
754
+
755
+ audio_sel = self.audio_list.curselection()
756
+ audio_track = audio_sel[0] if audio_sel else 0
757
+
758
+ if self.generate_single_var.get():
759
+ if not audio_sel:
760
+ messagebox.showwarning("Audio requerido", "Selecciona una pista de audio para la versión adicional")
761
+ return
762
+
763
+ extract_sub = self.extract_subs_var.get()
764
+ sub_sel = self.sub_list.curselection()
765
+ sub_track = sub_sel[0] if sub_sel else 0
766
+
767
+ self.is_converting = True
768
+ self.btn_start.config(state="disabled")
769
+ self.btn_cancel.config(state="normal")
770
+ self.progress_bar.start_pulse()
771
+
772
+ thread = threading.Thread(target=self._conversion_thread, args=(
773
+ self.files.copy(), self.urls.copy(), mode, output_dir, audio_track,
774
+ self.generate_single_var.get(), extract_sub, sub_track
775
+ ))
776
+ thread.daemon = True
777
+ thread.start()
778
+
779
+ def _conversion_thread(self, files, urls, mode, output_dir, audio_track, generate_single, extract_sub, sub_track):
780
+ all_sources = [(f, False) for f in files] + [(u, True) for u in urls]
781
+ total = len(all_sources)
782
+ completed = 0
783
+
784
+ for source, is_url in all_sources:
785
+ if not self.is_converting:
786
+ break
787
+
788
+ self.root.after(0, lambda: self._update_progress(0))
789
+ self.converter.convert(
790
+ source=source, is_url=is_url, mode=mode, output_dir=output_dir,
791
+ selected_audio=audio_track, generate_single=generate_single,
792
+ extract_sub=extract_sub and not is_url, sub_index=sub_track,
793
+ progress_cb=lambda p: self.root.after(0, lambda: self._update_progress(p)),
794
+ status_cb=lambda s: self.root.after(0, lambda: self._update_status(s))
795
+ )
796
+ completed += 1
797
+ self.root.after(0, lambda: self._update_status(f"Progreso total: {completed}/{total}"))
798
+
799
+ self.root.after(0, self._conversion_finished)
800
+
801
+ def _conversion_finished(self):
802
+ self.is_converting = False
803
+ self.btn_start.config(state="normal")
804
+ self.btn_cancel.config(state="disabled")
805
+ self.progress_bar.stop_pulse()
806
+ self._update_progress(100)
807
+ self._update_status("¡TODAS LAS CONVERSIONES COMPLETADAS!")
808
+ messagebox.showinfo("Éxito", f"Los videos convertidos están en la carpeta seleccionada:\n{self.output_dir_var.get()}")
809
+
810
+ def _cancel_conversion(self):
811
+ self.is_converting = False
812
+ self.converter.cancel()
813
+ self.progress_bar.stop_pulse()
814
+ self.btn_start.config(state="normal")
815
+ self.btn_cancel.config(state="disabled")
816
+ self._update_status("Conversión cancelada")
817
+
818
+ def run(self):
819
+ self.root.mainloop()
820
+
821
+ if __name__ == "__main__":
822
+ try:
823
+ creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
824
+ result = subprocess.run(["ffmpeg", "-version"], capture_output=True, creationflags=creationflags)
825
+ if result.returncode != 0:
826
+ raise Exception()
827
+ except:
828
+ root = tk.Tk()
829
+ root.withdraw()
830
+ messagebox.showerror("FFmpeg Requerido",
831
+ "FFmpeg no está instalado o no está en el PATH.\n\n"
832
+ "Descarga FFmpeg desde: https://ffmpeg.org/download.html")
833
+ sys.exit(1)
834
+
835
+ app = VideoConverterApp()
836
+ app.run()
selecciona segundo audio.bat ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+
4
+ :: Obt�n el directorio donde est� ubicado este script
5
+ set "script_dir=%~dp0"
6
+
7
+ :: Configura el directorio de salida
8
+ set "output_dir=%script_dir%output"
9
+
10
+ :: Aseg�rate de que el directorio de salida exista
11
+ if not exist "%output_dir%" mkdir "%output_dir%"
12
+
13
+ :: Verifica que se han pasado archivos como argumentos
14
+ if "%~1"=="" (
15
+ echo Por favor, pasa al menos un archivo MP4 como argumento.
16
+ exit /b 1
17
+ )
18
+
19
+ :: Procesa cada archivo MP4 pasado como argumento
20
+ :loop
21
+ if "%~1"=="" goto endloop
22
+
23
+ :: Verifica la extensi�n del archivo
24
+ if /i not "%~x1"==".mp4" (
25
+ echo El archivo "%~1" no es un archivo MP4.
26
+ shift
27
+ goto loop
28
+ )
29
+
30
+ set "input_file=%~1"
31
+ set "output_file=%output_dir%\%~n1_ES.mp4"
32
+
33
+ echo Procesando "%input_file%"...
34
+
35
+ :: Ejecuta ffmpeg para copiar el video y el segundo flujo de audio
36
+ ffmpeg -i "%input_file%" -map 0:v -c:v copy -map 0:a:1 -c:a copy "%output_file%" 2>&1
37
+
38
+ :: Verifica si ffmpeg se ejecut� con �xito
39
+ if !ERRORLEVEL! EQU 0 (
40
+ echo Conversi�n completada con �xito para "%input_file%".
41
+ ) else (
42
+ echo Error en la conversi�n para "%input_file%".
43
+ )
44
+
45
+ shift
46
+ goto loop
47
+
48
+ :endloop
49
+ echo Todos los archivos han sido procesados.
50
+ endlocal
51
+ pause