feat(global-search): implement global search functionality and manage recently viewed items

This commit is contained in:
4gray committed 2025-01-27 08:56:06 +01:00
1 parent a56e3594b4
commit 50df27f5ee
4 files changed
+237 -95

No files matched your search

+101
View File
@@ -30,6 +30,17 @@ export interface XtreamPlaylist {
type: string;
}
export interface GlobalSearchResult extends XtreamContent {
playlist_id: string;
playlist_name: string;
}
export interface GlobalRecentItem extends XtreamContent {
playlist_id: string;
playlist_name: string;
viewed_at: string;
}
@Injectable({
providedIn: 'root',
})
@@ -272,6 +283,96 @@ export class DatabaseService {
);
}
async globalSearchContent(
searchTerm: string,
types: string[]
): Promise<GlobalSearchResult[]> {
const db = await this.getConnection();
const placeholders = types.map(() => '?').join(',');
// Use a materialized subquery for better performance
return await db.select(
`
WITH filtered_content AS (
SELECT
c.id,
c.category_id,
c.title,
c.rating,
c.added,
c.poster_url,
c.xtream_id,
c.type,
cat.playlist_id,
p.name as playlist_name
FROM content c
INNER JOIN categories cat ON c.category_id = cat.id
INNER JOIN playlists p ON cat.playlist_id = p.id
WHERE c.type IN (${placeholders})
)
SELECT * FROM filtered_content
WHERE LOWER(title) LIKE LOWER(?)
ORDER BY title
LIMIT 50
`,
[...types, `%${searchTerm}%`]
);
}
async getGlobalRecentlyViewed(): Promise<GlobalRecentItem[]> {
try {
console.log('Starting getGlobalRecentlyViewed query...');
const db = await this.getConnection();
console.log('Got database connection');
// Check if table exists
const tableCheck = await db.select(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='recently_viewed'
`);
console.log('Tables check:', tableCheck);
const items = await db.select<GlobalRecentItem[]>(`
SELECT
c.id,
c.category_id,
c.title,
c.rating,
c.added,
c.poster_url,
c.xtream_id,
c.type,
cat.playlist_id,
p.name as playlist_name,
rv.viewed_at
FROM recently_viewed rv
INNER JOIN content c ON rv.content_id = c.id
INNER JOIN categories cat ON c.category_id = cat.id
INNER JOIN playlists p ON cat.playlist_id = p.id
ORDER BY rv.viewed_at DESC
LIMIT 100
`);
console.log('Query executed, items found:', items?.length);
console.log('First few items:', items?.slice(0, 3));
return items || [];
} catch (error) {
console.error('Detailed error in getGlobalRecentlyViewed:', error);
throw error; // Let's throw the error to see it in the component
}
}
async clearGlobalRecentlyViewed(): Promise<void> {
try {
const db = await this.getConnection();
await db.execute('DELETE FROM recently_viewed');
} catch (error) {
console.error('Error clearing global recently viewed:', error);
throw error;
}
}
async getPlaylistById(playlistId: string): Promise<XtreamPlaylist | null> {
const db = await this.getConnection();
const results = await db.select<XtreamPlaylist[]>(
+22
View File
@@ -143,6 +143,28 @@ export const withRecentItems = function () {
})
)
),
async loadGlobalRecentItems() {
try {
const items = await dbService.getGlobalRecentlyViewed();
patchState(store, {
recentItems: items || [],
});
} catch (error) {
console.error('Error loading global recent items:', error);
patchState(store, { recentItems: [] });
}
},
async clearGlobalRecentlyViewed() {
try {
await dbService.clearGlobalRecentlyViewed();
patchState(store, { recentItems: [] });
} catch (error) {
console.error(
'Error clearing global recently viewed:',
error
);
}
},
}))
);
};
+2
View File
@@ -2,6 +2,7 @@ import { XtreamCategory } from '../../../shared/xtream-category.interface';
import { XtreamLiveStream } from '../../../shared/xtream-live-stream.interface';
import { XtreamSerieItem } from '../../../shared/xtream-serie-item.interface';
import { XtreamVodStream } from '../../../shared/xtream-vod-stream.interface';
import { GlobalSearchResult } from '../services/database.service';
import { EpgItem } from '../xtream/epg-item.interface';
export type ContentType = 'live' | 'vod' | 'series';
@@ -39,4 +40,5 @@ export interface XtreamState {
epgItems: EpgItem[];
hideExternalInfoDialog: boolean;
portalStatus: PortalStatusType;
globalSearchResults: GlobalSearchResult[];
}
+112 -95
View File
@@ -53,6 +53,7 @@ const initialState: XtreamState = {
hideExternalInfoDialog:
localStorage.getItem('hideExternalInfoDialog') === 'true',
portalStatus: 'unavailable',
globalSearchResults: [],
};
/** to decode epg */
@@ -67,108 +68,105 @@ function b64DecodeUnicode(str: string) {
}
export const XtreamStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withRecentItems(),
withFavorites(),
withComputed((store) => {
return {
getCategoriesBySelectedType: computed(() => {
const type = store.selectedContentType();
return type === 'live'
? store.liveCategories()
: type === 'vod'
? store.vodCategories()
: store.serialCategories();
}),
getSelectedCategory: computed(() => {
const categoryId = store.selectedCategoryId();
return [
...store.vodCategories(),
...store.liveCategories(),
...store.serialCategories(),
].find((c) => c.id === categoryId);
}),
getImportCount: computed(() => store.importCount()),
getSelectedItemById: computed(() => {
const categoryType = store.selectedContentType();
const content =
categoryType === 'live'
? store.liveStreams()
: categoryType === 'vod'
? store.vodStreams()
: store.serialStreams();
withComputed((store) => ({
getCategoriesBySelectedType: computed(() => {
const type = store.selectedContentType();
return type === 'live'
? store.liveCategories()
: type === 'vod'
? store.vodCategories()
: store.serialCategories();
}),
getSelectedCategory: computed(() => {
const categoryId = store.selectedCategoryId();
return [
...store.vodCategories(),
...store.liveCategories(),
...store.serialCategories(),
].find((c) => c.id === categoryId);
}),
getImportCount: computed(() => store.importCount()),
getSelectedItemById: computed(() => {
const categoryType = store.selectedContentType();
const content =
categoryType === 'live'
? store.liveStreams()
: categoryType === 'vod'
? store.vodStreams()
: store.serialStreams();
if (!store.selectedItem()) return null;
if (!store.selectedItem()) return null;
return content.find(
(item) =>
(item as any).stream_id ===
store.selectedItem().stream_id ||
(item as any).id === store.selectedItem().id
);
}),
getPaginatedContent: computed(() => {
const startIndex = (store.page() - 1) * store.limit();
const endIndex = startIndex + store.limit();
const categoryId = store.selectedCategoryId();
return content.find(
(item) =>
(item as any).stream_id ===
store.selectedItem().stream_id ||
(item as any).id === store.selectedItem().id
);
}),
getPaginatedContent: computed(() => {
const startIndex = (store.page() - 1) * store.limit();
const endIndex = startIndex + store.limit();
const categoryId = store.selectedCategoryId();
if (!categoryId) {
return [];
}
if (!categoryId) {
return [];
}
const categoryType = store.selectedContentType();
const content =
categoryType === 'live'
? store.liveStreams()
: categoryType === 'vod'
? store.vodStreams()
: store.serialStreams();
const categoryType = store.selectedContentType();
const content =
categoryType === 'live'
? store.liveStreams()
: categoryType === 'vod'
? store.vodStreams()
: store.serialStreams();
return content
.filter((item) => item.category_id === categoryId)
.slice(startIndex, endIndex);
}),
getTotalPages: computed(() => {
const categoryId = store.selectedCategoryId();
if (!categoryId) return 0;
return content
.filter((item) => item.category_id === categoryId)
.slice(startIndex, endIndex);
}),
getTotalPages: computed(() => {
const categoryId = store.selectedCategoryId();
if (!categoryId) return 0;
const categoryType = store.selectedContentType();
const totalItems =
categoryType === 'live'
? store
.liveStreams()
.filter(
(i) => Number((i as any).id) === categoryId
).length
: categoryType === 'vod'
? store
.vodStreams()
.filter(
(i) => Number(i.category_id) === categoryId
).length
: store
.serialStreams()
.filter((i) => i.category_id === categoryId)
.length;
const categoryType = store.selectedContentType();
const totalItems =
categoryType === 'live'
? store
.liveStreams()
.filter((i) => Number((i as any).id) === categoryId)
.length
: categoryType === 'vod'
? store
.vodStreams()
.filter((i) => Number(i.category_id) === categoryId)
.length
: store
.serialStreams()
.filter((i) => i.category_id === categoryId).length;
return Math.ceil(totalItems / store.limit());
}),
selectItemsFromSelectedCategory: computed(() => {
const categoryId = store.selectedCategoryId();
const categoryType = store.selectedContentType();
const content =
categoryType === 'live'
? store.liveStreams()
: categoryType === 'vod'
? store.vodStreams()
: store.serialStreams();
return Math.ceil(totalItems / store.limit());
}),
selectItemsFromSelectedCategory: computed(() => {
const categoryId = store.selectedCategoryId();
const categoryType = store.selectedContentType();
const content =
categoryType === 'live'
? store.liveStreams()
: categoryType === 'vod'
? store.vodStreams()
: store.serialStreams();
return content.filter(
(item) => item.category_id === categoryId
);
}),
};
}),
return content.filter((item) => item.category_id === categoryId);
}),
globalRecentItems: computed(() => {
return store.recentItems(); // Remove the filter since we're getting global items directly from DB
}),
})),
withMethods(
(
store,
@@ -329,14 +327,18 @@ export const XtreamStore = signalStore(
const searchContent = async (
searchTerm: string,
types: string[]
types: string[],
route = inject(ActivatedRoute),
dbService = inject(DatabaseService)
) => {
if (!route.snapshot.params.id) return;
const results = await dbService.searchXtreamContent(
route.snapshot.params.id,
searchTerm,
types
);
patchState(store, { searchResults: results });
return results;
};
const fetchXtreamPlaylist = rxMethod<void>(
@@ -467,7 +469,13 @@ export const XtreamStore = signalStore(
}>(
pipe(
switchMap(({ term, types }) =>
from(searchContent(term, types))
from(searchContent(term, types, route, dbService)).pipe(
tap((results) => {
patchState(store, {
searchResults: results || [],
});
})
)
)
)
);
@@ -694,7 +702,10 @@ export const XtreamStore = signalStore(
});
},
resetSearchResults() {
patchState(store, { searchResults: [] });
patchState(store, {
searchResults: [],
globalSearchResults: [],
});
},
async loadEpg() {
console.log('Loading EPG...');
@@ -773,6 +784,12 @@ export const XtreamStore = signalStore(
patchState(store, { hideExternalInfoDialog });
},
checkPortalStatus,
setGlobalSearchResults(results: any[]) {
patchState(store, {
searchResults: results,
globalSearchResults: results,
});
},
};
}
)