refactor(electron): migrate from tauri/plugin-sql to libsql

This commit is contained in:
4gray committed 2025-10-18 21:31:29 +02:00
1 parent 6dcbbadda8
commit 0a934d2ac6
31 files changed
+3458 -752

No files matched your search

+28 -79
View File
@@ -1,8 +1,6 @@
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Params } from '@angular/router';
import { listen } from '@tauri-apps/api/event';
import { fetch } from '@tauri-apps/plugin-http';
import { DataService } from 'services';
import {
AUTO_UPDATE_PLAYLISTS,
@@ -27,23 +25,7 @@ export class ElectronService extends DataService {
constructor() {
super();
console.log('Tauri service initialized...');
this.setupEventListeners();
}
private async setupEventListeners() {
// Listen for player errors
/* this.eventListeners['player-error'] = await listen(
'player-error',
(event) => {
console.error('Player error:', event);
this.snackBar.open(
`Player Error: ${event.payload as string}`,
'Close',
{ duration: 5000 }
);
}
); */
console.log('Electron service initialized...');
}
getAppVersion(): string {
@@ -150,42 +132,22 @@ export class ElectronService extends DataService {
params: Record<string, string>;
}) {
try {
const url = new URL(payload.url);
Object.entries(payload.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
Cookie: `mac=${payload.macAddress}`,
},
});
if (!response.ok) {
throw new Error(
`Error: ${response.statusText} (Status: ${response.status})`
);
}
return await response.json();
/* window.postMessage({
type: 'STALKER_RESPONSE',
payload: result,
action: payload.params.action,
}); */
} catch (err) {
console.log(err);
// Use Electron IPC to make the Stalker request
const response = await window.electron.stalkerRequest(payload);
return response;
} catch (err: any) {
console.error('Stalker request error:', err);
window.postMessage({
type: ERROR,
message: err.message ?? 'Error: not found',
status: err.status ?? 404,
});
throw err;
}
}
private async fetchM3uPlaylistFromUrl(payload: Partial<Playlist>) {
console.log(payload);
window.electron.fetchPlaylistByUrl(payload.url).then((result) => {
// TODO: call store and decide and store where to store based on the isTemporary flag etc
window.postMessage({
@@ -255,36 +217,32 @@ export class ElectronService extends DataService {
url: string;
params: Record<string, string>;
}) {
let result: any;
const url = new URL(`${payload.url}/player_api.php`);
Object.entries(payload.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
try {
// Use Electron IPC to make the Xtream request
const response = await window.electron.xtreamRequest(payload);
const response = await fetch(url.toString());
const responseBody = await response.json();
if (!responseBody) {
result = {
type: ERROR,
status: response.status,
message: responseBody.message ?? 'Unknown error',
};
window.postMessage(result);
} else {
result = {
const result = {
type: XTREAM_RESPONSE,
payload: responseBody,
action: payload.params.action,
payload: response.payload,
action: response.action,
};
window.postMessage(result);
return result;
} catch (error: any) {
console.error('Xtream request error:', error);
const result = {
type: ERROR,
status: error.status ?? 500,
message: error.message ?? 'Failed to connect to Xtream server',
};
window.postMessage(result);
return result;
}
return result;
}
removeAllListeners(type: string): void {
if (type === 'all') {
// Unsubscribe from all Tauri events
// Unsubscribe from all event listeners
Object.values(this.eventListeners).forEach((unsubscribe) =>
unsubscribe()
);
@@ -296,7 +254,6 @@ export class ElectronService extends DataService {
}
// Also remove any window message listeners
// Note: This is a bit crude, but works for simple cases
window.removeEventListener('message', this.getListenerForCommand(type));
}
@@ -307,20 +264,12 @@ export class ElectronService extends DataService {
}
listenOn(command: string, callback: (...args: any[]) => void): void {
if (command.startsWith('tauri:')) {
// For Tauri specific events, use the Tauri event system
const tauriEvent = command.replace('tauri:', '');
listen(tauriEvent, callback).then((unsubscribe) => {
this.eventListeners[command] = unsubscribe;
});
} else {
// For backward compatibility, use window messages
window.addEventListener('message', callback);
}
// For Electron, use window message events
window.addEventListener('message', callback);
}
getAppEnvironment(): string {
return 'tauri';
return 'electron';
}
async fetchData(url: string, queryParams: Params) {
@@ -63,7 +63,8 @@
'CHANNELS.REMOVE_FAVORITE' | translate
"
(click)="
$event.stopPropagation(); removeFavorite(item)
$event.stopPropagation();
removeFromFavorites(item)
"
>
<mat-icon>delete</mat-icon>
@@ -78,5 +79,5 @@
</div>
}
</div>
<app-mpv-player-bar />
<!-- <app-mpv-player-bar /> -->
</div>
@@ -6,7 +6,6 @@ import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import { CategoryViewComponent } from '../../../xtream-tauri/category-view/category-view.component';
import { MpvPlayerBarComponent } from '../mpv-player-bar/mpv-player-bar.component';
@Component({
selector: 'app-favorites-layout',
@@ -21,7 +20,7 @@ import { MpvPlayerBarComponent } from '../mpv-player-bar/mpv-player-bar.componen
MatCardModule,
MatIcon,
MatIconButton,
MpvPlayerBarComponent,
/* MpvPlayerBarComponent, */
MatTooltip,
TranslatePipe,
],
@@ -33,15 +32,15 @@ export class FavoritesLayoutComponent {
readonly titleTranslationString = input<string>('CHANNELS.FAVORITES');
readonly categoryClicked = output<any>();
readonly removeFromFavorites = output<any>();
readonly removeFavorite = output<any>();
readonly openItem = output<any>();
setCategoryId(categoryId: any) {
this.categoryClicked.emit({ category_id: categoryId });
}
removeFavorite(item: any) {
this.removeFromFavorites.emit(item);
removeFromFavorites(item: any) {
this.removeFavorite.emit(item);
}
openFavorite(item: any) {
@@ -117,5 +117,5 @@
<div class="scrollable-content">
<router-outlet />
</div>
<app-mpv-player-bar />
<!-- <app-mpv-player-bar /> -->
</div>
@@ -8,7 +8,6 @@ import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { PlaylistsService } from 'services';
import { MpvPlayerBarComponent } from '../shared/components/mpv-player-bar/mpv-player-bar.component';
import { CategoryViewComponent } from '../xtream-tauri/category-view/category-view.component';
import { PlaylistErrorViewComponent } from '../xtream/playlist-error-view/playlist-error-view.component';
import { StalkerStore } from './stalker.store';
@@ -28,7 +27,7 @@ import { StalkerStore } from './stalker.store';
MatIconButton,
MatListModule,
MatPaginatorModule,
MpvPlayerBarComponent,
/* MpvPlayerBarComponent, */
NgxSkeletonLoaderModule,
PlaylistErrorViewComponent,
TranslatePipe,
@@ -99,7 +99,7 @@ export class FavoritesComponent implements OnInit {
async removeFromFavorites(item: any) {
await this.favoritesService.removeFromFavorites(
item.id,
item.content_id,
item.playlist_id
);
// Refresh favorites after removal
@@ -1,4 +1,5 @@
@let isEmbeddedPlayer = player() === 'videojs' || player() === 'html5';
@let isEmbeddedPlayer =
player() === 'videojs' || player() === 'html5' || player() === 'artplayer';
<div class="sidebar">
<div class="sidebar-header">
@@ -42,5 +43,5 @@
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
</div>
}
<app-mpv-player-bar />
<!-- <app-mpv-player-bar /> -->
</div>
@@ -54,3 +54,13 @@
font-size: 1.2rem;
font-weight: 500;
}
::ng-deep {
.video-player
> app-web-player-view
> app-art-player
> .artplayer-container {
height: 100% !important;
width: 100% !important;
}
}
@@ -15,7 +15,6 @@ import { TranslatePipe } from '@ngx-translate/core';
import { XtreamCategory } from 'shared-interfaces';
import { EpgViewComponent, WebPlayerViewComponent } from 'shared-portals';
import { SettingsStore } from '../../services/settings-store.service';
import { MpvPlayerBarComponent } from '../../shared/components/mpv-player-bar/mpv-player-bar.component';
import { CategoryViewComponent } from '../category-view/category-view.component';
import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component';
import { FavoritesService } from '../services/favorites.service';
@@ -34,7 +33,7 @@ import { XtreamStore } from '../xtream.store';
MatIconButton,
MatInputModule,
MatListModule,
MpvPlayerBarComponent,
/* MpvPlayerBarComponent, */
PortalChannelsListComponent,
TranslatePipe,
WebPlayerViewComponent,
@@ -6,4 +6,6 @@ export interface FavoriteItem {
stream_icon?: string;
poster_url?: string;
added_at?: string;
category_id: number;
xtream_id: number;
}
@@ -1,5 +1,5 @@
import { inject, Injectable } from '@angular/core';
import { from, map, mergeMap, Observable } from 'rxjs';
import { from, map, Observable } from 'rxjs';
import { DatabaseService } from 'services';
import { FavoriteItem } from './favorite-item.interface';
@@ -13,54 +13,34 @@ export class FavoritesService {
content_id: number;
playlist_id: string;
}): Promise<void> {
const db = await this.dbService.getConnection();
await db.execute(
`INSERT INTO favorites (content_id, playlist_id)
VALUES (?, ?)`,
[item.content_id, item.playlist_id]
);
await this.dbService.addToFavorites(item.content_id, item.playlist_id);
}
async removeFromFavorites(
contentId: number,
playlistId: string
): Promise<void> {
const db = await this.dbService.getConnection();
await db.execute(
`DELETE FROM favorites
WHERE content_id = ? AND playlist_id = ?`,
[contentId, playlistId]
);
await this.dbService.removeFromFavorites(contentId, playlistId);
}
async isFavorite(contentId: number, playlistId: string): Promise<boolean> {
const db = await this.dbService.getConnection();
const result = await db.select<any[]>(
`SELECT COUNT(*) as count
FROM favorites f
JOIN content c ON f.content_id = c.id
WHERE c.xtream_id = ? AND f.playlist_id = ?`,
[contentId, playlistId]
);
return result[0].count > 0;
return await this.dbService.isFavorite(contentId, playlistId);
}
getFavorites(playlistId: string): Observable<FavoriteItem[]> {
return from(this.dbService.getConnection()).pipe(
mergeMap(async (db) => {
return await db.select<FavoriteItem[]>(
`SELECT
c.*,
f.playlist_id,
f.added_at
FROM favorites f
JOIN content c ON f.content_id = c.id
WHERE f.playlist_id = ?
ORDER BY f.added_at DESC`,
[playlistId]
);
}),
map((results) => results)
return from(this.dbService.getFavorites(playlistId)).pipe(
map((items) =>
items.map((item) => ({
content_id: item.id,
playlist_id: playlistId,
type: item.type as 'live' | 'vod' | 'series',
title: item.title,
poster_url: item.poster_url,
added_at: item.added_at,
category_id: item.category_id,
xtream_id: item.xtream_id,
}))
)
);
}
}
@@ -20,45 +20,42 @@ export const withFavorites = function () {
favoritesService = inject(FavoritesService)
) => ({
async toggleFavorite(xtreamId: number, playlistId: string) {
let result = false;
if (!xtreamId || !playlistId) return;
const db = await dbService.getConnection();
const content: any = await db.select(
'SELECT content.id FROM content ' +
'INNER JOIN categories ON content.category_id = categories.id ' +
'WHERE content.xtream_id = ? AND categories.playlist_id = ?',
[xtreamId, playlistId]
);
if (!content || content.length === 0) {
console.error('Content not found in database');
return;
if (!xtreamId || !playlistId) {
return false;
}
const contentId = content[0].id;
const isFavorite = await favoritesService.isFavorite(
// Get content by xtream ID
const content = await dbService.getContentByXtreamId(
xtreamId,
playlistId
);
if (isFavorite) {
await favoritesService.removeFromFavorites(
contentId,
playlistId
if (!content) {
console.error(
'Content not found for xtream ID:',
xtreamId
);
result = false;
} else {
await favoritesService.addToFavorites({
content_id: contentId,
playlist_id: playlistId,
});
result = true;
return false;
}
patchState(store, { isFavorite: !isFavorite });
return result;
const currentStatus = store.isFavorite();
if (currentStatus) {
// Remove from favorites
await favoritesService.removeFromFavorites(
content.id,
playlistId
);
patchState(store, { isFavorite: false });
return false;
} else {
// Add to favorites
await favoritesService.addToFavorites({
content_id: content.id,
playlist_id: playlistId,
});
patchState(store, { isFavorite: true });
return true;
}
},
async checkFavoriteStatus(
@@ -70,13 +67,18 @@ export const withFavorites = function () {
return;
}
if (!xtreamId) {
// Get content by xtream ID
const content = await dbService.getContentByXtreamId(
xtreamId,
playlistId
);
if (!content) {
patchState(store, { isFavorite: false });
return;
}
const isFavorite = await favoritesService.isFavorite(
xtreamId,
content.id,
playlistId
);
@@ -18,6 +18,7 @@ export interface RecentlyViewedItem {
playlist_id: string;
viewed_at: string;
xtream_id: number;
category_id: number;
}
export const withRecentItems = function () {
@@ -29,30 +30,21 @@ export const withRecentItems = function () {
loadRecentItems: rxMethod<{ id: string }>(
pipe(
switchMap(async (playlist) => {
if (!playlist) return [];
console.log(
'Loading recent items for playlist',
const items = await dbService.getRecentItems(
playlist.id
);
const db = await dbService.getConnection();
return db.select<RecentlyViewedItem[]>(
`SELECT
rv.id,
c.title,
c.type,
c.poster_url,
c.id as content_id,
rv.playlist_id,
rv.viewed_at,
c.xtream_id,
c.category_id
FROM recently_viewed rv
JOIN content c ON rv.content_id = c.id
WHERE rv.playlist_id = ?
ORDER BY rv.viewed_at DESC
LIMIT 50`,
[playlist.id]
);
// Map to RecentlyViewedItem format
return items.map((item) => ({
id: item.id,
title: item.title,
type: item.type as 'live' | 'movie' | 'series',
poster_url: item.poster_url,
content_id: item.id,
playlist_id: playlist.id,
viewed_at: item.viewed_at || '',
xtream_id: item.xtream_id,
category_id: item.category_id,
}));
}),
tap((items: RecentlyViewedItem[]) =>
patchState(store, { recentItems: items })
@@ -67,50 +59,16 @@ export const withRecentItems = function () {
}>(
pipe(
switchMap(async ({ contentId, playlist }) => {
if (!playlist().id) {
console.error('No active playlist found');
return;
}
console.log(
'Adding to recently viewed:',
// contentId is actually xtream_id, need to look up the database content.id
const content = await dbService.getContentByXtreamId(
contentId,
playlist().id
);
const db = await dbService.getConnection();
const content: any = await db.select(
'SELECT content.id FROM content ' +
'INNER JOIN categories ON content.category_id = categories.id ' +
'WHERE content.xtream_id = ? AND categories.playlist_id = ?',
[contentId, playlist().id]
);
if (content && content.length > 0) {
// Check if item already exists in recently_viewed
const existing: any = await db.select(
'SELECT recently_viewed.id FROM recently_viewed ' +
'INNER JOIN content ON recently_viewed.content_id = content.id ' +
'INNER JOIN categories ON content.category_id = categories.id ' +
'WHERE content.id = ? AND categories.playlist_id = ?',
[content[0].id, playlist().id]
if (content) {
await dbService.addRecentItem(
content.id,
playlist().id
);
if (existing && existing.length > 0) {
// Update existing record's viewed_at timestamp
await db.execute(
'UPDATE recently_viewed SET viewed_at = CURRENT_TIMESTAMP WHERE id = ?',
[existing[0].id]
);
} else {
// Insert new record
await db.execute(
`INSERT INTO recently_viewed (content_id, playlist_id)
VALUES (?, ?)`,
[content[0].id, playlist().id]
);
}
return store.loadRecentItems({ id: playlist().id });
}
})
)
@@ -118,28 +76,30 @@ export const withRecentItems = function () {
clearRecentItems: rxMethod<{ id: string }>(
pipe(
switchMap(async (playlist) => {
console.log(
'Clearing recent items for playlist',
playlist.id
);
const db = await dbService.getConnection();
await db.execute(
`DELETE FROM recently_viewed WHERE playlist_id = ?`,
[playlist.id]
);
return store.loadRecentItems({ id: playlist.id });
await dbService.clearPlaylistRecentItems(playlist.id);
patchState(store, { recentItems: [] });
})
)
),
removeRecentItem: rxMethod<{ itemId: number; playlistId: string }>(
pipe(
switchMap(async ({ itemId, playlistId }) => {
const db = await dbService.getConnection();
await db.execute(
`DELETE FROM recently_viewed WHERE id = ? AND playlist_id = ?`,
[itemId, playlistId]
);
return store.loadRecentItems({ id: playlistId });
await dbService.removeRecentItem(itemId, playlistId);
// Reload recent items to update UI
const items =
await dbService.getRecentItems(playlistId);
const mappedItems = items.map((item) => ({
id: item.id,
title: item.title,
type: item.type as 'live' | 'movie' | 'series',
poster_url: item.poster_url,
content_id: item.id,
playlist_id: playlistId,
viewed_at: item.viewed_at || '',
xtream_id: item.xtream_id,
category_id: item.category_id,
}));
patchState(store, { recentItems: mappedItems });
})
)
),
@@ -30,5 +30,5 @@
<div class="scrollable-content">
<router-outlet />
</div>
<app-mpv-player-bar />
<!-- <app-mpv-player-bar /> -->
</div>
@@ -4,7 +4,6 @@ import { MatIcon } from '@angular/material/icon';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { XtreamCategory } from 'shared-interfaces';
import { MpvPlayerBarComponent } from '../shared/components/mpv-player-bar/mpv-player-bar.component';
import { CategoryViewComponent } from './category-view/category-view.component';
import { XtreamStore } from './xtream.store';
@@ -16,7 +15,7 @@ import { XtreamStore } from './xtream.store';
CategoryViewComponent,
TranslateModule,
RouterOutlet,
MpvPlayerBarComponent,
/* MpvPlayerBarComponent, */
MatIcon,
MatIconButton,
],