mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 06:06:18 -08:00
refactor: update nx monorepo structure
This commit is contained in:
1 parent
0ad1eeb785
commit
aa739f43e0
63 files changed
+1113
-427
No files matched your search
@@ -1,5 +1,3 @@
|
||||
export * from './lib/global-favorites/global-favorites-page.component';
|
||||
export * from './lib/workspace-context-panel/settings-context.service';
|
||||
export * from './lib/workspace-shell/workspace-shell-actions';
|
||||
export * from './lib/workspace-shell/workspace-shell.component';
|
||||
export * from './lib/workspace-sources/workspace-sources.component';
|
||||
+38
-15
@@ -32,6 +32,7 @@ import {
|
||||
Channel,
|
||||
EpgItem,
|
||||
EpgProgram,
|
||||
Playlist,
|
||||
ResolvedPortalPlayback,
|
||||
STALKER_REQUEST,
|
||||
StalkerPortalActions,
|
||||
@@ -41,6 +42,12 @@ import { GlobalFavoritesListComponent } from './global-favorites-list.component'
|
||||
import { GlobalFavoritesService } from './global-favorites.service';
|
||||
import { UnifiedFavoriteChannel } from './unified-favorite-channel.interface';
|
||||
|
||||
interface StalkerCreateLinkResponse {
|
||||
readonly js?: {
|
||||
readonly cmd?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-global-favorites-page',
|
||||
templateUrl: './global-favorites-page.component.html',
|
||||
@@ -248,7 +255,7 @@ export class GlobalFavoritesPageComponent implements OnInit {
|
||||
epgMap: Map<string, EpgProgram | null>,
|
||||
now: number
|
||||
): Promise<void> {
|
||||
let playlist: any;
|
||||
let playlist: Playlist | undefined;
|
||||
try {
|
||||
playlist = await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(playlistId)
|
||||
@@ -257,6 +264,10 @@ export class GlobalFavoritesPageComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playlist?.serverUrl || !playlist.username || !playlist.password) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
serverUrl: playlist.serverUrl,
|
||||
username: playlist.username,
|
||||
@@ -355,9 +366,16 @@ export class GlobalFavoritesPageComponent implements OnInit {
|
||||
}));
|
||||
}
|
||||
if (ch.sourceType === 'xtream' && ch.xtreamId) {
|
||||
const playlist = await firstValueFrom(
|
||||
const playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(ch.playlistId)
|
||||
);
|
||||
)) as Playlist | undefined;
|
||||
if (
|
||||
!playlist?.serverUrl ||
|
||||
!playlist.username ||
|
||||
!playlist.password
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return await this.xtreamApi.getShortEpg(
|
||||
{
|
||||
serverUrl: playlist.serverUrl,
|
||||
@@ -387,18 +405,23 @@ export class GlobalFavoritesPageComponent implements OnInit {
|
||||
private async resolveXtreamPlayback(
|
||||
ch: UnifiedFavoriteChannel
|
||||
): Promise<ResolvedPortalPlayback> {
|
||||
const playlist = await firstValueFrom(
|
||||
const playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(ch.playlistId)
|
||||
);
|
||||
)) as Playlist | undefined;
|
||||
if (
|
||||
ch.xtreamId == null ||
|
||||
!playlist?.serverUrl ||
|
||||
!playlist.username ||
|
||||
!playlist.password
|
||||
) {
|
||||
throw new Error('Missing Xtream playback credentials');
|
||||
}
|
||||
const credentials = {
|
||||
serverUrl: playlist.serverUrl,
|
||||
username: playlist.username,
|
||||
password: playlist.password,
|
||||
};
|
||||
const streamUrl = this.xtreamUrl.constructLiveUrl(
|
||||
credentials,
|
||||
ch.xtreamId!
|
||||
);
|
||||
const streamUrl = this.xtreamUrl.constructLiveUrl(credentials, ch.xtreamId);
|
||||
return {
|
||||
streamUrl,
|
||||
title: ch.name,
|
||||
@@ -409,13 +432,13 @@ export class GlobalFavoritesPageComponent implements OnInit {
|
||||
private async resolveStalkerPlayback(
|
||||
ch: UnifiedFavoriteChannel
|
||||
): Promise<ResolvedPortalPlayback> {
|
||||
const playlist = await firstValueFrom(
|
||||
const playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(ch.playlistId)
|
||||
);
|
||||
)) as Playlist | undefined;
|
||||
|
||||
const portalUrl =
|
||||
ch.stalkerPortalUrl ?? playlist.portalUrl ?? playlist.url;
|
||||
const macAddress = ch.stalkerMacAddress ?? playlist.macAddress;
|
||||
ch.stalkerPortalUrl ?? playlist?.portalUrl ?? playlist?.url ?? '';
|
||||
const macAddress = ch.stalkerMacAddress ?? playlist?.macAddress ?? '';
|
||||
const cmd = ch.stalkerCmd ?? '';
|
||||
|
||||
const params = {
|
||||
@@ -427,8 +450,8 @@ export class GlobalFavoritesPageComponent implements OnInit {
|
||||
JsHttpRequest: '1-xml',
|
||||
};
|
||||
|
||||
let response: any;
|
||||
if (playlist.isFullStalkerPortal) {
|
||||
let response: StalkerCreateLinkResponse | undefined;
|
||||
if (playlist?.isFullStalkerPortal && playlist) {
|
||||
response = await this.stalkerSession.makeAuthenticatedRequest(
|
||||
playlist,
|
||||
params
|
||||
|
||||
@@ -16,7 +16,12 @@ import { Store } from '@ngrx/store';
|
||||
import { selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import { firstValueFrom, map } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from 'services';
|
||||
import { Channel, PlaylistMeta } from 'shared-interfaces';
|
||||
import {
|
||||
Channel,
|
||||
Playlist,
|
||||
PlaylistMeta,
|
||||
StalkerPortalItem,
|
||||
} from 'shared-interfaces';
|
||||
import {
|
||||
buildFavoriteUid,
|
||||
UnifiedFavoriteChannel,
|
||||
@@ -24,6 +29,29 @@ import {
|
||||
|
||||
const GLOBAL_FAVORITES_ORDER_KEY = 'global-favorites-channel-order-v1';
|
||||
|
||||
interface XtreamGlobalFavoriteRow {
|
||||
readonly added_at?: string | null;
|
||||
readonly id: number;
|
||||
readonly playlist_id: string;
|
||||
readonly playlist_name: string;
|
||||
readonly position?: number | null;
|
||||
readonly poster_url?: string | null;
|
||||
readonly title: string;
|
||||
readonly xtream_id: number;
|
||||
}
|
||||
|
||||
type PlaylistWithChannels = Playlist & {
|
||||
readonly playlist?: {
|
||||
readonly items?: Channel[];
|
||||
};
|
||||
};
|
||||
|
||||
function isStalkerFavoriteItem(
|
||||
value: string | StalkerPortalItem
|
||||
): value is StalkerPortalItem {
|
||||
return typeof value !== 'string';
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GlobalFavoritesService {
|
||||
private readonly store = inject(Store);
|
||||
@@ -103,9 +131,14 @@ export class GlobalFavoritesService {
|
||||
|
||||
// Persist xtream positions to DB
|
||||
const xtreamUpdates = channels
|
||||
.filter((ch) => ch.sourceType === 'xtream' && ch.contentId != null)
|
||||
.filter(
|
||||
(
|
||||
ch
|
||||
): ch is UnifiedFavoriteChannel & { contentId: number } =>
|
||||
ch.sourceType === 'xtream' && ch.contentId != null
|
||||
)
|
||||
.map((ch, index) => ({
|
||||
content_id: ch.contentId!,
|
||||
content_id: ch.contentId,
|
||||
position: index,
|
||||
}));
|
||||
|
||||
@@ -138,16 +171,16 @@ export class GlobalFavoritesService {
|
||||
);
|
||||
|
||||
// Load full playlist to access channel items
|
||||
let playlist: any;
|
||||
let playlist: PlaylistWithChannels | undefined;
|
||||
try {
|
||||
playlist = await firstValueFrom(
|
||||
playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(meta._id)
|
||||
);
|
||||
)) as PlaylistWithChannels | undefined;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const items: Channel[] = playlist?.playlist?.items ?? [];
|
||||
const items = playlist?.playlist?.items ?? [];
|
||||
for (const ch of items) {
|
||||
if (favoriteIds.has(ch.id) || favoriteIds.has(ch.url)) {
|
||||
const sourceItemId = ch.url || ch.id;
|
||||
@@ -173,9 +206,10 @@ export class GlobalFavoritesService {
|
||||
private async getXtreamLiveFavorites(): Promise<UnifiedFavoriteChannel[]> {
|
||||
if (!window.electron) return [];
|
||||
|
||||
let items: any[] = [];
|
||||
let items: XtreamGlobalFavoriteRow[] = [];
|
||||
try {
|
||||
items = await this.dbService.getGlobalFavorites();
|
||||
items =
|
||||
(await this.dbService.getGlobalFavorites()) as XtreamGlobalFavoriteRow[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -211,17 +245,17 @@ export class GlobalFavoritesService {
|
||||
if (!meta.favorites?.length) continue;
|
||||
|
||||
// Fetch full playlist to get credentials
|
||||
let playlist: any;
|
||||
let playlist: Playlist | undefined;
|
||||
try {
|
||||
playlist = await firstValueFrom(
|
||||
playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(meta._id)
|
||||
);
|
||||
)) as Playlist | undefined;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const favorites = Array.isArray(playlist?.favorites)
|
||||
? playlist.favorites
|
||||
? playlist.favorites.filter(isStalkerFavoriteItem)
|
||||
: [];
|
||||
|
||||
for (const fav of favorites) {
|
||||
@@ -303,8 +337,9 @@ export class GlobalFavoritesService {
|
||||
const unordered: UnifiedFavoriteChannel[] = [];
|
||||
|
||||
for (const ch of channels) {
|
||||
if (orderMap.has(ch.uid)) {
|
||||
ordered.push({ ...ch, position: orderMap.get(ch.uid)! });
|
||||
const position = orderMap.get(ch.uid);
|
||||
if (position != null) {
|
||||
ordered.push({ ...ch, position });
|
||||
} else {
|
||||
unordered.push(ch);
|
||||
}
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export interface SettingsNavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsContextService {
|
||||
readonly sections = signal<SettingsNavItem[]>([]);
|
||||
readonly activeSection = signal<string>('general');
|
||||
readonly pendingScrollTarget = signal<string | null>(null);
|
||||
|
||||
setSections(items: SettingsNavItem[]): void {
|
||||
this.sections.set(items);
|
||||
}
|
||||
|
||||
setActiveSection(id: string): void {
|
||||
this.activeSection.set(id);
|
||||
}
|
||||
|
||||
navigateToSection(id: string): void {
|
||||
this.activeSection.set(id);
|
||||
this.pendingScrollTarget.set(id);
|
||||
}
|
||||
|
||||
clearPendingScrollTarget(): void {
|
||||
this.pendingScrollTarget.set(null);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.sections.set([]);
|
||||
this.activeSection.set('general');
|
||||
this.pendingScrollTarget.set(null);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -23,7 +23,7 @@ interface WorkspaceContextRoute {
|
||||
|
||||
interface XtreamCategoryLike {
|
||||
id?: number | string;
|
||||
category_id?: string;
|
||||
category_id?: number | string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
+10
-7
@@ -29,7 +29,7 @@ import { FavoritesContextService } from '@iptvnator/portal/shared/util';
|
||||
<app-category-view
|
||||
[items]="ctx.categories()"
|
||||
[selectedCategoryId]="ctx.selectedCategoryId()"
|
||||
(categoryClicked)="onCategoryClicked($any($event))"
|
||||
(categoryClicked)="onCategoryClicked($event)"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
@@ -42,22 +42,25 @@ export class WorkspaceFavoritesContextPanelComponent {
|
||||
const categories = this.ctx.categories();
|
||||
if (!categories.length) return 'Filter by type';
|
||||
const selected = categories.find(
|
||||
(c: any) => c.category_id === this.ctx.selectedCategoryId()
|
||||
) as any;
|
||||
(c) => c.category_id === this.ctx.selectedCategoryId()
|
||||
);
|
||||
return selected?.category_name ?? 'Filter by type';
|
||||
});
|
||||
|
||||
readonly countBadge = computed(() => {
|
||||
const categories = this.ctx.categories();
|
||||
const selected = categories.find(
|
||||
(c: any) => c.category_id === this.ctx.selectedCategoryId()
|
||||
) as any;
|
||||
(c) => c.category_id === this.ctx.selectedCategoryId()
|
||||
);
|
||||
if (!selected || selected.count === undefined) return '';
|
||||
return `${selected.count} ${selected.count === 1 ? 'item' : 'items'}`;
|
||||
});
|
||||
|
||||
onCategoryClicked(item: { category_id?: string | number }): void {
|
||||
const id = String(item.category_id ?? 'all');
|
||||
onCategoryClicked(item: {
|
||||
category_id?: string | number;
|
||||
id?: string | number;
|
||||
}): void {
|
||||
const id = String(item.category_id ?? item.id ?? 'all');
|
||||
this.ctx.setCategoryId(id);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Location } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SettingsContextService } from './settings-context.service';
|
||||
import { SettingsContextService } from '@iptvnator/workspace/shell/util';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-settings-context-panel',
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { PlaylistType } from 'components';
|
||||
|
||||
export interface WorkspaceAccountInfoData {
|
||||
vodStreamsCount: number;
|
||||
liveStreamsCount: number;
|
||||
seriesCount: number;
|
||||
}
|
||||
|
||||
export interface WorkspaceShellActions {
|
||||
openAddPlaylistDialog(type: PlaylistType): void;
|
||||
openGlobalSearch(initialQuery?: string): void;
|
||||
openGlobalRecent(): void;
|
||||
openAccountInfo(data: WorkspaceAccountInfoData): void;
|
||||
}
|
||||
|
||||
export const WORKSPACE_SHELL_ACTIONS =
|
||||
new InjectionToken<WorkspaceShellActions>('WORKSPACE_SHELL_ACTIONS');
|
||||
+6
-4
@@ -14,16 +14,18 @@ import {
|
||||
PlaylistsService,
|
||||
SettingsStore,
|
||||
} from 'services';
|
||||
import { SettingsContextService } from '../workspace-context-panel/settings-context.service';
|
||||
import {
|
||||
SettingsContextService,
|
||||
WORKSPACE_SHELL_ACTIONS,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import { WorkspaceShellComponent } from './workspace-shell.component';
|
||||
import { WORKSPACE_SHELL_ACTIONS } from './workspace-shell-actions';
|
||||
|
||||
class MockXtreamStore {
|
||||
readonly contentSortMode = signal<
|
||||
'date-desc' | 'date-asc' | 'name-asc' | 'name-desc'
|
||||
>('date-desc');
|
||||
readonly getCategoryItemCounts = signal(new Map<number, number>());
|
||||
readonly recentItems = signal<any[]>([]);
|
||||
readonly recentItems = signal<unknown[]>([]);
|
||||
|
||||
setContentSortMode = jest.fn();
|
||||
setSearchTerm = jest.fn();
|
||||
@@ -33,7 +35,7 @@ class MockXtreamStore {
|
||||
|
||||
describe('WorkspaceShellComponent action matrix', () => {
|
||||
beforeEach(() => {
|
||||
(window as any).electron = { platform: 'darwin' };
|
||||
window.electron = { platform: 'darwin' } as typeof window.electron;
|
||||
|
||||
const selectSignal = jest
|
||||
.fn()
|
||||
|
||||
@@ -48,20 +48,20 @@ import {
|
||||
PlaylistsService,
|
||||
SettingsStore,
|
||||
} from 'services';
|
||||
import {
|
||||
SettingsContextService,
|
||||
WorkspaceAccountInfoData,
|
||||
WORKSPACE_SHELL_ACTIONS,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import {
|
||||
WorkspaceCommandItem,
|
||||
WorkspaceCommandPaletteComponent,
|
||||
WorkspaceCommandSelection,
|
||||
} from '../workspace-command-palette/workspace-command-palette.component';
|
||||
import { SettingsContextService } from '../workspace-context-panel/settings-context.service';
|
||||
import { WorkspaceContextPanelComponent } from '../workspace-context-panel/workspace-context-panel.component';
|
||||
import { WorkspaceFavoritesContextPanelComponent } from '../workspace-context-panel/workspace-favorites-context-panel.component';
|
||||
import { WorkspaceSettingsContextPanelComponent } from '../workspace-context-panel/workspace-settings-context-panel.component';
|
||||
import { WorkspaceSourcesFiltersPanelComponent } from '../workspace-sources-filters-panel/workspace-sources-filters-panel.component';
|
||||
import {
|
||||
WorkspaceAccountInfoData,
|
||||
WORKSPACE_SHELL_ACTIONS,
|
||||
} from './workspace-shell-actions';
|
||||
|
||||
interface WorkspaceContext {
|
||||
provider: 'xtreams' | 'stalker' | 'playlists';
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { Store } from '@ngrx/store';
|
||||
import { PlaylistType, RecentPlaylistsComponent } from 'components';
|
||||
import { selectActiveTypeFilters, selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import { map } from 'rxjs';
|
||||
import { WORKSPACE_SHELL_ACTIONS } from '../workspace-shell/workspace-shell-actions';
|
||||
import { WORKSPACE_SHELL_ACTIONS } from '@iptvnator/workspace/shell/util';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-sources',
|
||||
|
||||
Reference in new issue
Block a user