Skip to content

Commit b3554c1

Browse files
authored
Merge pull request #3 from ChanMo/release/v0.8.19
release: v0.8.19 Radio 睡眠定时器功能
2 parents 64d481b + 41ea87f commit b3554c1

8 files changed

Lines changed: 1062 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "TikLocal"
3-
version = "0.8.18"
3+
version = "0.8.19"
44
description = "A local media server that combines the features of TikTok and Pinterest"
55
authors = ["ChanMo <chan.mo@outlook.com>"]
66
readme = "README.md"

tiklocal/app.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from flask import Flask, render_template, send_from_directory, request, redirect, send_file
1010

1111
# Service Imports
12-
from tiklocal.services import LibraryService, FavoriteService, RecommendService, IMAGE_EXTENSIONS
12+
from tiklocal.services import LibraryService, FavoriteService, RecommendService, IMAGE_EXTENSIONS, AUDIO_EXTENSIONS
1313
from tiklocal.services.thumbnail import ThumbnailService
1414
from tiklocal.services.metadata import (
1515
ImageMetadataStore,
@@ -284,6 +284,11 @@ def tiktok():
284284
"""Immersive Mixed Media Feed"""
285285
return render_template('tiktok.html', menu='index')
286286

287+
@app.route('/radio')
288+
def radio_view():
289+
"""Audio Radio Player"""
290+
return render_template('radio.html', menu='radio')
291+
287292
@app.route('/download')
288293
def download_view():
289294
"""URL Download Center"""
@@ -519,6 +524,31 @@ def thumb_view():
519524

520525

521526
# --- API Routes ---
527+
@app.route('/api/radio/items')
528+
def api_radio_items():
529+
offset = _read_int_arg('offset', 0, minimum=0)
530+
limit = _read_int_arg('limit', 200, minimum=1, maximum=500)
531+
audios = library_service.scan_audios()
532+
favorites = favorite_service.load()
533+
total = len(audios)
534+
page = audios[offset:offset + limit]
535+
items = []
536+
for p in page:
537+
name = library_service.get_relative_path(p)
538+
items.append({
539+
'name': name,
540+
'media_url': f'/media/{quote(name, safe="/")}',
541+
'thumb_url': f'/thumb?uri={quote(name, safe="")}',
542+
'title': p.stem,
543+
'duration': None,
544+
'is_favorite': name in favorites,
545+
})
546+
return {'success': True, 'data': {
547+
'items': items,
548+
'total': total,
549+
'has_more': offset + limit < total,
550+
}}
551+
522552
@app.route('/api/feed/mix')
523553
def api_feed_mix():
524554
page = _read_int_arg('page', 1, minimum=1)

tiklocal/services/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
VIDEO_EXTENSIONS = {'.mp4', '.webm', '.mov', '.mkv', '.avi', '.m4v'}
1010
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}
11+
AUDIO_EXTENSIONS = {'.mp3', '.flac', '.aac', '.m4a', '.ogg', '.opus', '.wav'}
1112
FAVORITE_FILENAME = 'favorite.json'
1213

1314
class LibraryService:
@@ -45,6 +46,17 @@ def scan_videos(self, recursive=True) -> list[Path]:
4546
except Exception:
4647
return []
4748

49+
def scan_audios(self, recursive=True) -> list[Path]:
50+
"""Scan for audio files."""
51+
pattern = '**/*' if recursive else '*'
52+
audios = []
53+
try:
54+
for ext in AUDIO_EXTENSIONS:
55+
audios.extend(self.media_root.glob(f"{pattern}{ext}"))
56+
return sorted(audios, key=lambda p: p.stat().st_mtime, reverse=True)
57+
except Exception:
58+
return []
59+
4860
def scan_images(self, recursive=True) -> list[Path]:
4961
"""Scan for image files."""
5062
pattern = '**/*' if recursive else '*'

tiklocal/services/thumbnail.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from pathlib import Path
77
from tiklocal.paths import get_thumbnails_dir, get_thumbs_map_path
88

9+
AUDIO_EXTENSIONS = {'.mp3', '.flac', '.aac', '.m4a', '.ogg', '.opus', '.wav'}
10+
911
class ThumbnailService:
1012
def __init__(self, media_root: Path):
1113
self.media_root = media_root
@@ -36,8 +38,21 @@ def get_thumbnail(self, rel_path: str) -> tuple[Path | bytes, str]:
3638
return self.placeholder, 'image/png'
3739

3840
def _generate(self, video_path: Path, output_path: Path, timestamp: float = None) -> bool:
41+
suffix = video_path.suffix.lower()
42+
43+
# Audio: extract embedded cover art
44+
if suffix in AUDIO_EXTENSIONS:
45+
cmd = ['ffmpeg', '-i', str(video_path), '-an', '-vframes', '1', str(output_path), '-y']
46+
try:
47+
sp.run(cmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL, timeout=30)
48+
if output_path.exists() and output_path.stat().st_size > 0:
49+
return True
50+
except Exception:
51+
pass
52+
return False
53+
3954
candidates = [timestamp] if timestamp is not None else [5.0, 1.0, 0.1]
40-
55+
4156
for t in candidates:
4257
cmd = [
4358
'ffmpeg', '-y',

tiklocal/static/input.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,29 @@ video, img, canvas, svg {
6161
@apply bg-gray-400 dark:bg-gray-500;
6262
}
6363

64+
/* Radio 波形动画 */
65+
@keyframes wave-bar {
66+
0%, 100% { transform: scaleY(0.3); }
67+
50% { transform: scaleY(1); }
68+
}
69+
.audio-waveform {
70+
display: flex;
71+
gap: 4px;
72+
align-items: center;
73+
justify-content: center;
74+
}
75+
.audio-waveform span {
76+
width: 5px;
77+
border-radius: 3px;
78+
background: currentColor;
79+
animation: wave-bar 1s ease-in-out infinite;
80+
}
81+
.audio-waveform span:nth-child(2) { animation-delay: 0.1s; }
82+
.audio-waveform span:nth-child(3) { animation-delay: 0.2s; }
83+
.audio-waveform span:nth-child(4) { animation-delay: 0.3s; }
84+
.audio-waveform span:nth-child(5) { animation-delay: 0.4s; }
85+
.audio-waveform.paused span { animation-play-state: paused; }
86+
6487
/* 焦点环优化 */
6588
.focus-ring {
6689
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800;

0 commit comments

Comments
 (0)