mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
feat(global-widgets): add M3U recently-viewed and favs support
- Introduced M3uRecentlyViewedItem interface to define structure for recently viewed items. - Implemented utility functions to handle mapping and building of recently viewed items from playlists. - Updated Playlist interface to use PlaylistRecentlyViewedItem type for recently viewed items. - Enhanced PortalActivityItem interface to include 'm3u' as a possible source. - Added UI components for displaying recently viewed channels, including clear functionality. - Integrated recent view functionality into the channel list container and sidebar components. - Updated dashboard data service to manage M3U favorites and recently viewed items. - Added tests to ensure correct behavior of recently viewed items in the dashboard data service.
This commit is contained in:
1 parent
188ebb6fd9
commit
1e9f5d9d05
27 files changed
+1286
-227
No files matched your search
@@ -4,11 +4,11 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import { of } from 'rxjs';
|
||||
import { DatabaseService } from 'services';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
import { DatabaseService, PlaylistsService } from 'services';
|
||||
import { Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
import { DashboardDataService } from './dashboard-data.service';
|
||||
|
||||
describe('DashboardDataService.matchesScope', () => {
|
||||
describe('DashboardDataService', () => {
|
||||
let service: DashboardDataService;
|
||||
|
||||
const playlistsSignal = signal<PlaylistMeta[]>([
|
||||
@@ -18,6 +18,22 @@ describe('DashboardDataService.matchesScope', () => {
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
favorites: ['channel-1', 'https://example.com/stream-2.m3u8'],
|
||||
recentlyViewed: [
|
||||
{
|
||||
source: 'm3u',
|
||||
id: 'https://example.com/stream-1.m3u8',
|
||||
url: 'https://example.com/stream-1.m3u8',
|
||||
title: 'Channel One',
|
||||
channel_id: 'channel-1',
|
||||
poster_url: 'https://example.com/logo-1.png',
|
||||
tvg_id: 'tvg-1',
|
||||
tvg_name: 'Channel One',
|
||||
group_title: 'News',
|
||||
category_id: 'live',
|
||||
added_at: '2026-02-01T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: 'xtream-1',
|
||||
@@ -36,19 +52,90 @@ describe('DashboardDataService.matchesScope', () => {
|
||||
}
|
||||
return signal(null);
|
||||
}),
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
|
||||
const dbServiceMock = {
|
||||
getGlobalRecentlyViewed: jest.fn(),
|
||||
getGlobalFavorites: jest.fn(),
|
||||
getGlobalRecentlyViewed: jest.fn().mockResolvedValue([]),
|
||||
getGlobalFavorites: jest.fn().mockResolvedValue([]),
|
||||
removeFromFavorites: jest.fn().mockResolvedValue(undefined),
|
||||
removeRecentItem: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const playlistMock: Playlist = {
|
||||
_id: 'm3u-1',
|
||||
title: 'M3U Playlist',
|
||||
count: 2,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
lastUsage: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
favorites: ['channel-1', 'https://example.com/stream-2.m3u8'],
|
||||
playlist: {
|
||||
items: [
|
||||
{
|
||||
id: 'channel-1',
|
||||
name: 'Channel One',
|
||||
url: 'https://example.com/stream-1.m3u8',
|
||||
tvg: {
|
||||
logo: 'https://example.com/logo-1.png',
|
||||
id: 'tvg-1',
|
||||
name: 'Channel One',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'channel-2',
|
||||
name: 'Channel Two',
|
||||
url: 'https://example.com/stream-2.m3u8',
|
||||
tvg: {
|
||||
logo: 'https://example.com/logo-2.png',
|
||||
id: 'tvg-2',
|
||||
name: 'Channel Two',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as Playlist;
|
||||
const playlistsServiceMock = {
|
||||
getPlaylistById: jest.fn().mockReturnValue(of(playlistMock)),
|
||||
setFavorites: jest.fn().mockReturnValue(of(undefined)),
|
||||
removeFromM3uRecentlyViewed: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
of({
|
||||
...playlistMock,
|
||||
recentlyViewed: [],
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
playlistsServiceMock.getPlaylistById.mockClear();
|
||||
playlistsServiceMock.getPlaylistById.mockReturnValue(of(playlistMock));
|
||||
playlistsServiceMock.setFavorites.mockClear();
|
||||
playlistsServiceMock.setFavorites.mockReturnValue(of(undefined));
|
||||
playlistsServiceMock.removeFromM3uRecentlyViewed.mockClear();
|
||||
playlistsServiceMock.removeFromM3uRecentlyViewed.mockReturnValue(
|
||||
of({
|
||||
...playlistMock,
|
||||
recentlyViewed: [],
|
||||
})
|
||||
);
|
||||
dbServiceMock.getGlobalFavorites.mockClear();
|
||||
dbServiceMock.getGlobalFavorites.mockResolvedValue([]);
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockClear();
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([]);
|
||||
dbServiceMock.removeFromFavorites.mockClear();
|
||||
dbServiceMock.removeRecentItem.mockClear();
|
||||
storeMock.dispatch.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DashboardDataService,
|
||||
{ provide: Store, useValue: storeMock },
|
||||
{ provide: DatabaseService, useValue: dbServiceMock },
|
||||
{
|
||||
provide: PlaylistsService,
|
||||
useValue: playlistsServiceMock,
|
||||
},
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: {
|
||||
@@ -102,4 +189,118 @@ describe('DashboardDataService.matchesScope', () => {
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('includes M3U favorites in global favorite items', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
|
||||
expect(service.globalFavoriteItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'channel-1',
|
||||
title: 'Channel One',
|
||||
type: 'live',
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
poster_url: 'https://example.com/logo-1.png',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'https://example.com/stream-2.m3u8',
|
||||
title: 'Channel Two',
|
||||
type: 'live',
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
poster_url: 'https://example.com/logo-2.png',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('builds the M3U favorites route', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
const m3uItem = service
|
||||
.globalFavoriteItems()
|
||||
.find((item) => item.source === 'm3u');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
expect(service.getGlobalFavoriteLink(m3uItem!)).toEqual([
|
||||
'/workspace',
|
||||
'playlists',
|
||||
'm3u-1',
|
||||
'favorites',
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes M3U favorites via PlaylistsService', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
const m3uItem = service
|
||||
.globalFavoriteItems()
|
||||
.find((item) => item.id === 'channel-1');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
|
||||
await service.removeGlobalFavorite(m3uItem!);
|
||||
|
||||
expect(playlistsServiceMock.setFavorites).toHaveBeenCalledWith(
|
||||
'm3u-1',
|
||||
['https://example.com/stream-2.m3u8']
|
||||
);
|
||||
});
|
||||
|
||||
it('matches M3U provider scope', () => {
|
||||
const result = service.matchesScope('m3u-1', 'm3u', {
|
||||
providers: ['m3u'],
|
||||
playlistIds: [],
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('includes M3U recently viewed items in global recent items', () => {
|
||||
expect(service.globalRecentItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'https://example.com/stream-1.m3u8',
|
||||
title: 'Channel One',
|
||||
type: 'live',
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
xtream_id: 'https://example.com/stream-1.m3u8',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('builds the M3U recent route with navigation state', () => {
|
||||
const m3uItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'm3u');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
expect(service.getRecentItemLink(m3uItem!)).toEqual([
|
||||
'/workspace',
|
||||
'playlists',
|
||||
'm3u-1',
|
||||
'recent',
|
||||
]);
|
||||
expect(service.getRecentItemNavigationState(m3uItem!)).toEqual({
|
||||
openRecentChannelUrl: 'https://example.com/stream-1.m3u8',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes M3U recently viewed via PlaylistsService', async () => {
|
||||
const m3uItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'm3u');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
|
||||
await service.removeGlobalRecentItem(m3uItem!);
|
||||
|
||||
expect(
|
||||
playlistsServiceMock.removeFromM3uRecentlyViewed
|
||||
).toHaveBeenCalledWith(
|
||||
'm3u-1',
|
||||
'https://example.com/stream-1.m3u8'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
selectActivePlaylist,
|
||||
selectAllPlaylistsMeta,
|
||||
} from 'm3u-state';
|
||||
import { startWith } from 'rxjs';
|
||||
import { DatabaseService } from 'services';
|
||||
import { firstValueFrom, startWith } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from 'services';
|
||||
import {
|
||||
buildPlaylistRecentItems,
|
||||
Channel,
|
||||
Playlist,
|
||||
PlaylistMeta,
|
||||
PortalActivityType,
|
||||
PortalFavoriteItem,
|
||||
@@ -18,7 +21,6 @@ import {
|
||||
} from 'shared-interfaces';
|
||||
import {
|
||||
buildStalkerFavoriteItems,
|
||||
buildStalkerRecentItems,
|
||||
getActivityTypeLabelKey,
|
||||
mapDbFavoriteToItem,
|
||||
mapDbRecentToItem,
|
||||
@@ -46,6 +48,7 @@ export type DashboardFavoriteItem = PortalFavoriteItem;
|
||||
export class DashboardDataService {
|
||||
private readonly store = inject(Store);
|
||||
private readonly dbService = inject(DatabaseService);
|
||||
private readonly playlistsService = inject(PlaylistsService);
|
||||
private readonly ngZone = inject(NgZone);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageTick = toSignal(
|
||||
@@ -57,19 +60,27 @@ export class DashboardDataService {
|
||||
private readonly xtreamGlobalFavorites = signal<DashboardFavoriteItem[]>(
|
||||
[]
|
||||
);
|
||||
private readonly m3uGlobalFavorites = signal<DashboardFavoriteItem[]>([]);
|
||||
readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta);
|
||||
readonly activePlaylist = this.store.selectSignal(selectActivePlaylist);
|
||||
|
||||
readonly stalkerGlobalRecentItems = computed<GlobalRecentItem[]>(() => {
|
||||
readonly playlistBackedGlobalRecentItems = computed<GlobalRecentItem[]>(
|
||||
() => {
|
||||
this.languageTick();
|
||||
return buildStalkerRecentItems(
|
||||
this.playlists(),
|
||||
this.translateText('WORKSPACE.DASHBOARD.STALKER_PORTAL')
|
||||
);
|
||||
});
|
||||
return buildPlaylistRecentItems(this.playlists(), {
|
||||
stalker: this.translateText(
|
||||
'WORKSPACE.DASHBOARD.STALKER_PORTAL'
|
||||
),
|
||||
m3u: this.translateText('WORKSPACE.DASHBOARD.M3U'),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
readonly globalRecentItems = computed<GlobalRecentItem[]>(() =>
|
||||
[...this.xtreamGlobalRecentItems(), ...this.stalkerGlobalRecentItems()]
|
||||
[
|
||||
...this.xtreamGlobalRecentItems(),
|
||||
...this.playlistBackedGlobalRecentItems(),
|
||||
]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
toDateTimestamp(b.viewed_at) - toDateTimestamp(a.viewed_at)
|
||||
@@ -86,15 +97,19 @@ export class DashboardDataService {
|
||||
});
|
||||
|
||||
readonly globalFavoriteItems = computed(() =>
|
||||
[...this.xtreamGlobalFavorites(), ...this.stalkerGlobalFavorites()]
|
||||
[
|
||||
...this.xtreamGlobalFavorites(),
|
||||
...this.m3uGlobalFavorites(),
|
||||
...this.stalkerGlobalFavorites(),
|
||||
]
|
||||
.sort((a, b) => toTimestamp(b.added_at) - toTimestamp(a.added_at))
|
||||
.slice(0, 200)
|
||||
);
|
||||
|
||||
constructor() {
|
||||
void this.reloadGlobalFavorites();
|
||||
if (window.electron) {
|
||||
void this.reloadGlobalRecentItems();
|
||||
void this.reloadGlobalFavorites();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +157,13 @@ export class DashboardDataService {
|
||||
}
|
||||
|
||||
async reloadGlobalFavorites(): Promise<void> {
|
||||
const xtreamReload = this.reloadXtreamGlobalFavorites();
|
||||
const m3uReload = this.reloadM3uGlobalFavorites();
|
||||
|
||||
await Promise.all([xtreamReload, m3uReload]);
|
||||
}
|
||||
|
||||
private async reloadXtreamGlobalFavorites(): Promise<void> {
|
||||
if (!window.electron) {
|
||||
this.xtreamGlobalFavorites.set([]);
|
||||
return;
|
||||
@@ -162,6 +184,38 @@ export class DashboardDataService {
|
||||
}
|
||||
}
|
||||
|
||||
private async reloadM3uGlobalFavorites(): Promise<void> {
|
||||
const m3uPlaylists = this.playlists().filter(
|
||||
(playlist) =>
|
||||
!playlist.serverUrl &&
|
||||
!playlist.macAddress &&
|
||||
Array.isArray(playlist.favorites) &&
|
||||
playlist.favorites.some(
|
||||
(favorite): favorite is string =>
|
||||
typeof favorite === 'string' && favorite.trim().length > 0
|
||||
)
|
||||
);
|
||||
|
||||
if (m3uPlaylists.length === 0) {
|
||||
this.ngZone.run(() => this.m3uGlobalFavorites.set([]));
|
||||
return;
|
||||
}
|
||||
|
||||
const favoriteItems = await Promise.all(
|
||||
m3uPlaylists.map((playlist) =>
|
||||
this.loadM3uPlaylistFavorites(playlist).catch(
|
||||
(): DashboardFavoriteItem[] => []
|
||||
)
|
||||
)
|
||||
);
|
||||
const flattenedFavorites = favoriteItems.reduce(
|
||||
(acc, items) => [...acc, ...items],
|
||||
[] as DashboardFavoriteItem[]
|
||||
);
|
||||
|
||||
this.ngZone.run(() => this.m3uGlobalFavorites.set(flattenedFavorites));
|
||||
}
|
||||
|
||||
isTypeInKind(
|
||||
type: PortalActivityType,
|
||||
kind: DashboardContentKind
|
||||
@@ -256,6 +310,9 @@ export class DashboardDataService {
|
||||
if (item.source === 'xtream') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.XTREAM');
|
||||
}
|
||||
if (item.source === 'm3u') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.M3U');
|
||||
}
|
||||
return this.translateText('WORKSPACE.DASHBOARD.PROVIDER');
|
||||
}
|
||||
|
||||
@@ -285,36 +342,37 @@ export class DashboardDataService {
|
||||
}
|
||||
|
||||
if (item.source === 'stalker') {
|
||||
const playlist = this.playlists().find(
|
||||
(p) => p._id === item.playlist_id
|
||||
);
|
||||
if (!playlist) return;
|
||||
|
||||
const currentRecent = Array.isArray(
|
||||
(playlist as { recentlyViewed?: unknown[] }).recentlyViewed
|
||||
)
|
||||
? [
|
||||
...((playlist as { recentlyViewed?: unknown[] })
|
||||
.recentlyViewed ?? []),
|
||||
]
|
||||
: [];
|
||||
|
||||
const itemMatchStr = String(item.id);
|
||||
const filteredRecent = currentRecent.filter(
|
||||
(raw, index) =>
|
||||
!stalkerItemMatchesId(
|
||||
raw,
|
||||
itemMatchStr,
|
||||
playlist._id,
|
||||
index
|
||||
)
|
||||
const updatedPlaylist = await firstValueFrom(
|
||||
this.playlistsService.removeFromPortalRecentlyViewed(
|
||||
item.playlist_id,
|
||||
item.id
|
||||
)
|
||||
);
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: item.playlist_id,
|
||||
recentlyViewed: filteredRecent,
|
||||
recentlyViewed: updatedPlaylist?.recentlyViewed ?? [],
|
||||
} as unknown as PlaylistMeta,
|
||||
}) as any
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.source === 'm3u') {
|
||||
const updatedPlaylist = await firstValueFrom(
|
||||
this.playlistsService.removeFromM3uRecentlyViewed(
|
||||
item.playlist_id,
|
||||
String(item.xtream_id ?? item.id)
|
||||
)
|
||||
);
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: {
|
||||
_id: item.playlist_id,
|
||||
recentlyViewed: updatedPlaylist?.recentlyViewed ?? [],
|
||||
} as unknown as PlaylistMeta,
|
||||
}) as any
|
||||
);
|
||||
@@ -330,6 +388,9 @@ export class DashboardDataService {
|
||||
if (item.source === 'xtream') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.XTREAM');
|
||||
}
|
||||
if (item.source === 'm3u') {
|
||||
return this.translateText('WORKSPACE.DASHBOARD.M3U');
|
||||
}
|
||||
return this.translateText('WORKSPACE.DASHBOARD.PROVIDER');
|
||||
}
|
||||
|
||||
@@ -388,6 +449,29 @@ export class DashboardDataService {
|
||||
}) as any
|
||||
);
|
||||
}
|
||||
|
||||
if (item.source === 'm3u') {
|
||||
const playlist = await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(item.playlist_id)
|
||||
);
|
||||
const currentFavorites = Array.isArray(playlist?.favorites)
|
||||
? playlist.favorites.filter(
|
||||
(favorite): favorite is string =>
|
||||
typeof favorite === 'string'
|
||||
)
|
||||
: [];
|
||||
const filteredFavorites = currentFavorites.filter(
|
||||
(favorite) => favorite !== String(item.id)
|
||||
);
|
||||
|
||||
await firstValueFrom(
|
||||
this.playlistsService.setFavorites(
|
||||
item.playlist_id,
|
||||
filteredFavorites
|
||||
)
|
||||
);
|
||||
await this.reloadGlobalFavorites();
|
||||
}
|
||||
}
|
||||
|
||||
formatTimestamp(value?: string | number): string {
|
||||
@@ -422,6 +506,73 @@ export class DashboardDataService {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private async loadM3uPlaylistFavorites(
|
||||
playlistMeta: PlaylistMeta
|
||||
): Promise<DashboardFavoriteItem[]> {
|
||||
const playlist = (await firstValueFrom(
|
||||
this.playlistsService.getPlaylistById(playlistMeta._id)
|
||||
)) as Playlist & {
|
||||
playlist?: {
|
||||
items?: Channel[];
|
||||
};
|
||||
};
|
||||
const favorites = Array.isArray(playlist?.favorites)
|
||||
? playlist.favorites.filter(
|
||||
(favorite): favorite is string =>
|
||||
typeof favorite === 'string' && favorite.trim().length > 0
|
||||
)
|
||||
: [];
|
||||
|
||||
if (favorites.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: Channel[] = Array.isArray(playlist?.playlist?.items)
|
||||
? (playlist.playlist.items as Channel[])
|
||||
: [];
|
||||
const fallbackTimestamp =
|
||||
this.getM3uFavoriteTimestamp(playlistMeta) ??
|
||||
new Date(0).toISOString();
|
||||
|
||||
return items.reduce<DashboardFavoriteItem[]>((acc, channel) => {
|
||||
const channelId = String(channel.id ?? '').trim();
|
||||
const channelUrl = String(channel.url ?? '').trim();
|
||||
const matchedFavoriteId = favorites.find(
|
||||
(favorite) =>
|
||||
favorite === channelId || favorite === channelUrl
|
||||
);
|
||||
|
||||
if (!matchedFavoriteId) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
id: matchedFavoriteId,
|
||||
title: channel.name?.trim() || channel.tvg?.name || channelId,
|
||||
type: 'live',
|
||||
playlist_id: playlistMeta._id,
|
||||
playlist_name:
|
||||
playlistMeta.title || playlistMeta.filename || 'M3U',
|
||||
added_at: fallbackTimestamp,
|
||||
category_id: 'live',
|
||||
xtream_id: matchedFavoriteId,
|
||||
poster_url: channel.tvg?.logo || undefined,
|
||||
source: 'm3u',
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
private getM3uFavoriteTimestamp(playlist: PlaylistMeta): string | null {
|
||||
if (playlist.updateDate) {
|
||||
return new Date(playlist.updateDate).toISOString();
|
||||
}
|
||||
|
||||
return typeof playlist.importDate === 'string'
|
||||
? playlist.importDate
|
||||
: null;
|
||||
}
|
||||
|
||||
private getRecentTimestamp(item: PlaylistMeta): number {
|
||||
return toTimestamp(item.updateDate) || toTimestamp(item.importDate);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,15 @@ export function getRecentItemNavigation(
|
||||
};
|
||||
}
|
||||
|
||||
if (item.source === 'm3u') {
|
||||
return {
|
||||
link: ['/workspace', 'playlists', item.playlist_id, 'recent'],
|
||||
state: {
|
||||
openRecentChannelUrl: String(item.xtream_id ?? item.id ?? ''),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return buildXtreamNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
@@ -52,6 +61,12 @@ export function getGlobalFavoriteNavigation(
|
||||
};
|
||||
}
|
||||
|
||||
if (item.source === 'm3u') {
|
||||
return {
|
||||
link: ['/workspace', 'playlists', item.playlist_id, 'favorites'],
|
||||
};
|
||||
}
|
||||
|
||||
return buildXtreamNavigationTarget({
|
||||
playlistId: item.playlist_id,
|
||||
type: item.type,
|
||||
|
||||
Reference in new issue
Block a user