Upload 11 files
Browse files- .gitattributes +5 -0
- 2 audios.bat +247 -0
- New Text Document.bat +152 -0
- c.exe +3 -0
- c.py +684 -0
- ffmpeg.exe +3 -0
- ffplay.exe +3 -0
- ffprobe.exe +3 -0
- job.bat +168 -0
- job.exe +3 -0
- job.py +836 -0
- 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
|