feat: update icons, add suffix and fix bugs
- Removed shadow from URL entry wrap in app.html for a cleaner look. - Updated various icon files to improve visual consistency across the application. - Add suffix to identify different files of same video - Fixed invisible download for short link Co-authored-by: Copilot <copilot@github.com>
|
|
@ -1,7 +1,7 @@
|
|||
# MyTube
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
MyTube is a self-hosted web UI for `yt-dlp`, for downloading media from YouTube and [dozens of other sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md).
|
||||
|
||||
|
|
|
|||
33
app/main.py
|
|
@ -509,10 +509,25 @@ def _public_visit_from_environ(environ: dict) -> str:
|
|||
class Notifier(DownloadQueueNotifier):
|
||||
async def added(self, dl):
|
||||
log.info(f"Notifier: Download added - {dl.title}")
|
||||
visit_id = public_download_owner.get(dl.url)
|
||||
visit_id = None
|
||||
candidates = [getattr(dl, 'url', None)]
|
||||
entry = getattr(dl, 'entry', None) or {}
|
||||
if isinstance(entry, dict):
|
||||
for key in ('url', 'webpage_url', 'original_url'):
|
||||
v = entry.get(key)
|
||||
if v:
|
||||
candidates.append(v)
|
||||
for cand in candidates:
|
||||
if not cand:
|
||||
continue
|
||||
visit_id = public_download_owner.get(cand)
|
||||
if visit_id:
|
||||
break
|
||||
|
||||
if visit_id:
|
||||
public_download_owner_by_id[dl.id] = visit_id
|
||||
_register_public_download_key(visit_id, getattr(dl, 'key', None))
|
||||
|
||||
await _emit_download_event('added', serializer.encode(dl), url=dl.url, dl_id=dl.id, dl_key=getattr(dl, 'key', None))
|
||||
|
||||
async def updated(self, dl):
|
||||
|
|
@ -797,6 +812,22 @@ async def add(request):
|
|||
if not visit_id:
|
||||
raise web.HTTPBadRequest(reason='missing X-Visit-Id in public mode')
|
||||
_register_public_download(visit_id, o['url'])
|
||||
# Also attempt to extract metadata for the URL and register the
|
||||
# canonical webpage_url (if different). This ensures short links
|
||||
# (eg. youtu.be) and yt-dlp-normalised links both map to the same
|
||||
# public visit so socket events are delivered.
|
||||
try:
|
||||
extractor = getattr(dqueue, '_DownloadQueue__extract_info', None)
|
||||
if extractor is not None:
|
||||
loop = asyncio.get_running_loop()
|
||||
entry = await loop.run_in_executor(None, lambda: extractor(o['url'], o.get('ytdl_options_presets'), o.get('ytdl_options_overrides')))
|
||||
if isinstance(entry, dict):
|
||||
webpage = entry.get('webpage_url') or entry.get('url') or entry.get('original_url')
|
||||
if webpage and webpage != o['url']:
|
||||
_register_public_download(visit_id, webpage)
|
||||
except Exception:
|
||||
# Extraction failure shouldn't block the add operation; log and continue.
|
||||
log.debug('Could not extract entry to register canonical public URL', exc_info=True)
|
||||
status = await dqueue.add(
|
||||
o['url'],
|
||||
o['download_type'],
|
||||
|
|
|
|||
60
app/ytdl.py
|
|
@ -919,7 +919,58 @@ class DownloadQueue:
|
|||
dldirectory, error_message = self.__calc_download_path(dl.download_type, dl.folder)
|
||||
if error_message is not None:
|
||||
return error_message
|
||||
output = self.config.OUTPUT_TEMPLATE if len(dl.custom_name_prefix) == 0 else f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
|
||||
suffix = None
|
||||
if not getattr(dl, 'custom_name_prefix', None):
|
||||
parts = []
|
||||
if getattr(dl, 'quality', ''):
|
||||
parts.append(str(dl.quality))
|
||||
codec_value = getattr(dl, 'codec', '')
|
||||
if str(codec_value).lower() == 'auto':
|
||||
entry = getattr(dl, 'entry', None) or {}
|
||||
resolved = None
|
||||
if isinstance(entry, dict):
|
||||
vc = entry.get('vcodec')
|
||||
ac = entry.get('acodec')
|
||||
if vc:
|
||||
resolved = vc
|
||||
elif ac:
|
||||
resolved = ac
|
||||
else:
|
||||
for fld in ('requested_formats', 'formats'):
|
||||
lst = entry.get(fld) or []
|
||||
if isinstance(lst, list):
|
||||
for it in lst:
|
||||
if isinstance(it, dict):
|
||||
if it.get('vcodec'):
|
||||
resolved = it.get('vcodec')
|
||||
break
|
||||
if it.get('acodec'):
|
||||
resolved = it.get('acodec')
|
||||
break
|
||||
if resolved:
|
||||
break
|
||||
if resolved:
|
||||
s = str(resolved).lower()
|
||||
if 'avc' in s or 'h264' in s:
|
||||
codec_value = 'h264'
|
||||
elif 'hevc' in s or 'h265' in s or 'h.265' in s:
|
||||
codec_value = 'h265'
|
||||
elif 'av01' in s or 'av1' in s:
|
||||
codec_value = 'av1'
|
||||
elif 'vp9' in s or 'vp09' in s:
|
||||
codec_value = 'vp9'
|
||||
else:
|
||||
codec_value = re.sub(r'[^0-9a-zA-Z]+', '_', s)
|
||||
|
||||
if codec_value:
|
||||
parts.append(str(codec_value))
|
||||
if parts:
|
||||
suffix = '_'.join(_sanitize_path_component(p) for p in parts if p)
|
||||
|
||||
if getattr(dl, 'custom_name_prefix', None):
|
||||
output = self.config.OUTPUT_TEMPLATE if len(dl.custom_name_prefix) == 0 else f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
|
||||
else:
|
||||
output = self.config.OUTPUT_TEMPLATE
|
||||
output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER
|
||||
entry = getattr(dl, 'entry', None)
|
||||
if entry is not None and entry.get('playlist_index') is not None:
|
||||
|
|
@ -932,6 +983,13 @@ class DownloadQueue:
|
|||
output = self.config.OUTPUT_TEMPLATE_CHANNEL
|
||||
sanitized = {k: _sanitize_path_component(v) for k, v in entry.items()}
|
||||
output = _resolve_outtmpl_fields(output, sanitized, ('channel',))
|
||||
if suffix:
|
||||
token = '%(ext)s'
|
||||
idx = output.rfind(token)
|
||||
if idx != -1:
|
||||
output = output[:idx] + f'{suffix}.{token}'
|
||||
else:
|
||||
output = f'{output}.{suffix}'
|
||||
ytdl_options = self._build_ytdl_options(
|
||||
getattr(dl, 'ytdl_options_presets', None),
|
||||
getattr(dl, 'ytdl_options_overrides', {}) or {},
|
||||
|
|
|
|||
822
metube.ai
BIN
mytube.af
Normal file
|
|
@ -131,7 +131,7 @@
|
|||
}
|
||||
</ng-template>
|
||||
|
||||
<div class="url-entry-wrap shadow-sm">
|
||||
<div class="url-entry-wrap">
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
BIN
ui/src/assets/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 773 B After Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 10 KiB |
|
|
@ -1,22 +1,26 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="500.000000pt" height="500.000000pt" viewBox="0 0 500.000000 500.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1945 4260 c-698 -17 -1097 -40 -1273 -76 -406 -81 -580 -284 -649
|
||||
-754 -21 -142 -30 -1102 -14 -1484 17 -392 44 -570 111 -728 103 -241 278
|
||||
-360 620 -422 224 -41 740 -65 1400 -66 405 0 1617 27 1800 40 284 21 532 72
|
||||
670 137 227 108 341 372 380 883 13 167 13 1199 0 1392 -22 331 -54 488 -131
|
||||
647 -87 178 -213 279 -411 330 -128 33 -435 66 -848 91 -235 15 -1208 20
|
||||
-1655 10z m1315 -956 c0 -3 -169 -293 -375 -644 -206 -351 -375 -641 -375
|
||||
-644 0 -3 169 -6 375 -6 l375 0 0 -160 0 -160 -760 0 -760 0 0 160 0 160 376
|
||||
0 c309 0 375 2 371 13 -3 8 -172 298 -376 645 -204 347 -371 633 -371 636 0 3
|
||||
342 6 760 6 418 0 760 -3 760 -6z"/>
|
||||
</g>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 2084 2084" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="页面-1" serif:id="页面 1" transform="matrix(4.166667,0,0,4.166667,0,0)">
|
||||
<rect x="0" y="0" width="500" height="500" style="fill:none;"/>
|
||||
<clipPath id="_clip1">
|
||||
<rect x="0" y="0" width="500" height="500"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<g id="OBJECTS">
|
||||
<g transform="matrix(1,0,0,1,499.6328,304.6911)">
|
||||
<path d="M0,-109.738C-0.204,-115.659 -0.511,-121.576 -0.964,-127.488C-2.885,-152.548 -5.678,-181.251 -22.236,-201.297C-36.406,-218.452 -58.356,-222.331 -79.193,-224.689C-105.579,-227.675 -132.089,-229.477 -158.62,-230.49C-212.357,-232.543 -266.146,-231.353 -319.888,-230.158C-347.498,-229.545 -375.126,-228.93 -402.656,-226.738C-436.461,-224.047 -471.232,-219.093 -486.979,-185.311C-501.114,-154.988 -499.359,-116.49 -499.538,-83.867C-499.698,-54.615 -499.795,-25.351 -498.623,3.877C-497.101,41.813 -494.849,91.229 -453.9,107.866C-432.039,116.748 -405.644,118.732 -382.296,120.25C-357.548,121.86 -332.737,121.666 -307.952,122.034C-255.973,122.805 -204.005,121.023 -152.042,119.991C-115.95,119.276 -78.904,118.869 -44.366,107.084C-14.554,96.911 -6.775,64.602 -2.743,36.576C1.263,8.73 0.242,-19.523 0.196,-47.565C0.163,-68.28 0.713,-89.03 0,-109.738" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,250.018,169.591)">
|
||||
<path d="M-6.726,118.019C-5.325,120.403 -2.766,121.868 -0.001,121.867C2.765,121.867 5.324,120.402 6.724,118.018C19.488,96.286 47.744,48.179 67.813,14.01C69.501,11.134 69.521,7.574 67.865,4.68C66.208,1.786 63.128,-0 59.793,-0C30.557,0 -30.586,0 -59.824,0C-63.16,-0 -66.24,1.786 -67.896,4.681C-69.553,7.576 -69.532,11.136 -67.842,14.012C-47.732,48.235 -19.488,96.302 -6.726,118.019Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(-1,0,0,1,500.001,129.465)">
|
||||
<path d="M316.759,169.592C319.226,169.592 321.591,170.572 323.335,172.316C325.079,174.06 326.059,176.425 326.059,178.892L326.059,191.643C326.059,194.11 325.079,196.475 323.335,198.219C321.591,199.963 319.226,200.943 316.759,200.943L183.242,200.943C180.775,200.943 178.41,199.963 176.666,198.219C174.922,196.475 173.942,194.11 173.942,191.643L173.942,178.892C173.942,176.425 174.922,174.06 176.666,172.316C178.41,170.572 180.775,169.592 183.242,169.592L316.759,169.592Z" style="fill:white;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(500.000263,-500,353.826308,353.826494,-499.632753,195.3089)"><stop offset="0" style="stop-color:rgb(126,101,231);stop-opacity:1"/><stop offset="0.49" style="stop-color:rgb(224,82,105);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,76,65);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -13,8 +13,8 @@
|
|||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"src": "assets/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
|
|
|
|||