feat:[v1.7]增加重复url通知

This commit is contained in:
tigeren 2025-11-24 16:11:37 +00:00
parent b042805d46
commit f5024383bf
7 changed files with 216 additions and 4 deletions

View File

@ -1,5 +1,5 @@
docker build -t 192.168.2.212:3000/tigeren/metube:1.6 .
docker build -t 192.168.2.212:3000/tigeren/metube:1.7 .
docker push 192.168.2.212:3000/tigeren/metube:1.6
docker push 192.168.2.212:3000/tigeren/metube:1.7
docker compose up -d --build --force-recreate

View File

@ -156,6 +156,10 @@ class Notifier(DownloadQueueNotifier):
async def cleared(self, id):
log.info(f"Notifier: Download cleared - {id}")
await sio.emit('cleared', serializer.encode(id))
async def event(self, event):
log.info(f"Notifier: Event - {event['type']}")
await sio.emit('event', serializer.encode(event))
dqueue = DownloadQueue(config, Notifier())
app.on_startup.append(lambda app: dqueue.initialize())
@ -433,6 +437,16 @@ def version(request):
"version": os.getenv("METUBE_VERSION", "dev")
})
@routes.get(config.URL_PREFIX + 'events')
def get_events(request):
events = dqueue.get_events()
return web.Response(text=serializer.encode(events))
@routes.post(config.URL_PREFIX + 'events/clear')
async def clear_events(request):
dqueue.clear_events()
return web.Response(text=serializer.encode({'status': 'ok'}))
if config.URL_PREFIX != '/':
@routes.get('/')
def index_redirect_root(request):

View File

@ -32,6 +32,9 @@ class DownloadQueueNotifier:
async def cleared(self, id):
raise NotImplementedError
async def event(self, event):
raise NotImplementedError
class DownloadInfo:
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error, entry, playlist_item_limit):
@ -369,6 +372,10 @@ class DownloadQueue:
self.reserved_filenames = set() # Track filenames being processed
self.precheck_in_progress = {} # Track URL -> DownloadInfo for items in precheck queue
# Event notifications (keep last 5 in memory)
self.events = [] # List of {type, message, timestamp, url}
self.max_events = 5
self.done.load()
async def __import_queue(self):
@ -621,6 +628,8 @@ class DownloadQueue:
self.pending.exists(dl.url) or
self.done.exists(dl.url)):
log.info(f"[PreCheck] URL already queued/processing/downloaded, skipping: {dl.url}")
# Add event notification
self._add_event('duplicate_skipped', 'URL already in queue or downloaded', dl.url)
return {'status': 'ok', 'msg': 'Download already exists'}
dldirectory, error_message = self.__calc_download_path(dl.quality, dl.format, dl.folder)
@ -878,3 +887,26 @@ class DownloadQueue:
list((k, v.info) for k, v in self.queue.items()) +
list((k, v.info) for k, v in self.pending.items()),
list((k, v.info) for k, v in self.done.items()))
def _add_event(self, event_type, message, url=None):
"""Add an event to the events list (keep only last 5)."""
event = {
'type': event_type,
'message': message,
'timestamp': int(time.time()),
'url': url
}
self.events.append(event)
# Keep only last 5 events
if len(self.events) > self.max_events:
self.events = self.events[-self.max_events:]
# Notify frontend via WebSocket
asyncio.create_task(self.notifier.event(event))
def get_events(self):
"""Get all events (last 5)."""
return self.events
def clear_events(self):
"""Clear all events."""
self.events = []

View File

@ -67,6 +67,41 @@
</nav>
<main role="main" class="container container-xl">
<!-- Events Display Area -->
<div *ngIf="events.length > 0" class="events-container mb-3">
<div class="events-header">
<div class="d-flex align-items-center justify-content-between">
<h6 class="mb-0">
<fa-icon [icon]="faClock" class="me-2"></fa-icon>
Recent Events
</h6>
<button type="button"
class="btn btn-sm btn-outline-secondary"
(click)="clearEvents()"
title="Clear all events">
<fa-icon [icon]="faTrashAlt" class="me-1"></fa-icon>
Clear
</button>
</div>
</div>
<div class="events-list">
<div *ngFor="let event of events" class="event-item" [ngClass]="'event-' + event.type">
<div class="event-icon">
<fa-icon [icon]="faTimesCircle"></fa-icon>
</div>
<div class="event-content">
<div class="event-message">{{ event.message }}</div>
<div class="event-url" *ngIf="event.url" [title]="event.url">
{{ event.url }}
</div>
</div>
<div class="event-time">
{{ getRelativeTime(event.timestamp) }}
</div>
</div>
</div>
</div>
<form #f="ngForm">
<div class="container add-url-box">
<!-- Main URL Input with Download Button -->

View File

@ -236,3 +236,86 @@ main
fa-icon
vertical-align: middle
// Events display area
.events-container
background: var(--bs-body-bg)
border: 1px solid var(--bs-border-color)
border-radius: 0.5rem
overflow: hidden
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1)
margin-top: 1rem
.events-header
padding: 0.75rem 1rem
background: var(--bs-secondary-bg)
border-bottom: 1px solid var(--bs-border-color)
h6
color: var(--bs-secondary-color)
font-weight: 600
display: flex
align-items: center
fa-icon
opacity: 0.7
.events-list
max-height: 300px
overflow-y: auto
.event-item
display: flex
align-items: flex-start
padding: 0.875rem 1rem
border-bottom: 1px solid var(--bs-border-color)
transition: background-color 0.2s
&:last-child
border-bottom: none
&:hover
background: var(--bs-tertiary-bg)
.event-icon
flex-shrink: 0
width: 32px
height: 32px
display: flex
align-items: center
justify-content: center
border-radius: 50%
margin-right: 0.75rem
fa-icon
font-size: 1.1rem
&.event-duplicate_skipped
.event-icon
background: rgba(255, 193, 7, 0.15)
color: #ffc107
.event-content
flex: 1
min-width: 0
.event-message
font-size: 0.9375rem
color: var(--bs-body-color)
margin-bottom: 0.25rem
font-weight: 500
.event-url
font-size: 0.8125rem
color: var(--bs-secondary-color)
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
font-family: 'Courier New', monospace
.event-time
flex-shrink: 0
font-size: 0.75rem
color: var(--bs-secondary-color)
margin-left: 0.75rem
whitespace: nowrap

View File

@ -1,4 +1,4 @@
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { faTrashAlt, faCheckCircle, faTimesCircle, IconDefinition } from '@fortawesome/free-regular-svg-icons';
import { faRedoAlt, faSun, faMoon, faCircleHalfStroke, faCheck, faExternalLinkAlt, faDownload, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt } from '@fortawesome/free-solid-svg-icons';
@ -18,7 +18,7 @@ import {KeyValue} from "@angular/common";
styleUrls: ['./app.component.sass'],
standalone: false
})
export class AppComponent implements AfterViewInit {
export class AppComponent implements OnInit, AfterViewInit {
addUrl: string;
formats: Format[] = Formats;
qualities: Quality[];
@ -78,6 +78,9 @@ export class AppComponent implements AfterViewInit {
faClock = faClock;
faTachometerAlt = faTachometerAlt;
// Events from backend (last 5 events)
events: Array<{type: string, message: string, timestamp: number, url: string}> = [];
constructor(public downloads: DownloadsService, private cookieService: CookieService, private http: HttpClient) {
this.format = cookieService.get('metube_format') || 'any';
// Needs to be set or qualities won't automatically be set
@ -98,6 +101,15 @@ export class AppComponent implements AfterViewInit {
this.downloads.updated.subscribe(() => {
this.updateMetrics();
});
// Subscribe to events
this.downloads.eventReceived.subscribe((event: any) => {
console.debug('Event received in component:', event);
// Add to events array (keep last 5)
this.events.push(event);
if (this.events.length > 5) {
this.events = this.events.slice(-5);
}
});
}
ngOnInit() {
@ -106,6 +118,9 @@ export class AppComponent implements AfterViewInit {
this.customDirs$ = this.getMatchingCustomDir();
this.setTheme(this.activeTheme);
// Load events from backend
this.loadEvents();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (this.activeTheme.id === 'auto') {
this.setTheme(this.activeTheme);
@ -269,11 +284,38 @@ export class AppComponent implements AfterViewInit {
alert(`Error adding URL: ${status.msg}`);
} else {
this.addUrl = '';
// Reload events after adding
this.loadEvents();
}
this.addInProgress = false;
});
}
// Load events from backend
loadEvents() {
this.http.get('/events').subscribe((events: any[]) => {
this.events = events || [];
});
}
// Clear all events
clearEvents() {
this.http.post('/events/clear', {}).subscribe(() => {
this.events = [];
});
}
// Format timestamp to relative time
getRelativeTime(timestamp: number): string {
const now = Date.now() / 1000;
const diff = Math.floor(now - timestamp);
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return `${Math.floor(diff / 86400)}d ago`;
}
downloadItemByKey(id: string) {
this.downloads.startById([id]).subscribe();
}

View File

@ -44,6 +44,7 @@ export class DownloadsService {
ytdlOptionsChanged = new Subject();
configurationChanged = new Subject();
updated = new Subject();
eventReceived = new Subject(); // New subject for events
configuration = {};
customDirs = {};
@ -112,6 +113,11 @@ export class DownloadsService {
let data = JSON.parse(strdata);
this.ytdlOptionsChanged.next(data);
});
socket.fromEvent('event').subscribe((strdata: string) => {
let event = JSON.parse(strdata);
console.debug('Received event:', event);
this.eventReceived.next(event);
});
}
handleHTTPError(error: HttpErrorResponse) {