feat:[v1.7]增加重复url通知
This commit is contained in:
parent
b042805d46
commit
f5024383bf
|
|
@ -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
|
||||
14
app/main.py
14
app/main.py
|
|
@ -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):
|
||||
|
|
|
|||
32
app/ytdl.py
32
app/ytdl.py
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue