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>
This commit is contained in:
TonyBlur 2026-05-04 14:14:26 +08:00
parent eb43d4655d
commit c755f394d1
17 changed files with 120 additions and 849 deletions

View file

@ -1,7 +1,7 @@
# MyTube
![Build Status](https://github.com/TonyBlur/mytube/actions/workflows/main.yml/badge.svg)
![Docker Pulls](https://img.shields.io/docker/pulls/tonyblur/mytube.svg)
![Docker Pulls](https://img.shields.io/docker/pulls/tonyblu/mytube.svg)
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).

View file

@ -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'],

View file

@ -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

File diff suppressed because one or more lines are too long

BIN
mytube.af Normal file

Binary file not shown.

View file

@ -131,7 +131,7 @@
}
</ng-template>
<div class="url-entry-wrap shadow-sm">
<div class="url-entry-wrap">
<textarea
autocomplete="off"
spellcheck="false"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 B

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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"
}
],