Ctrl K
ComunidadGuías…

src/renderer/features/player/utils.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { QueryClient } from '@tanstack/react-query';

import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { PlayerFilter, useSettingsStore } from '/@/renderer/store';
import { sortSongList } from '/@/shared/api/utils';
import {
    PlaylistSongListQuery,
    PlaylistSongListQueryClientSide,
    Song,
    SongDetailQuery,
    SongListQuery,
    SongListResponse,
    SongListSort,
    SortOrder,
} from '/@/shared/types/domain-types';
import { LogCategory, logFn } from '/@/shared/utils/logger';
import { logMsg } from '/@/shared/utils/logger-message';

const SONG_LIST_GC_MS = 1000 * 60;

const fetchSongList = async (
    queryClient: QueryClient,
    serverId: string,
    queryFilter: SongListQuery,
) => {
    const queryKey = queryKeys.songs.list(serverId, queryFilter);

    return queryClient.fetchQuery({
        gcTime: SONG_LIST_GC_MS,
        queryFn: async ({ signal }) =>
            api.controller.getSongList({
                apiClientProps: {
                    serverId,
                    signal,
                },
                query: queryFilter,
            }),
        queryKey,
        staleTime: SONG_LIST_GC_MS,
    });
};

export const getPlaylistSongsById = async (args: {
    id: string;
    query?: Partial<PlaylistSongListQueryClientSide>;
    queryClient: QueryClient;
    serverId: string;
}) => {
    const { id, query, queryClient, serverId } = args;

    const queryFilter: PlaylistSongListQuery = {
        id,
    };

    const queryKey = queryKeys.playlists.songList(serverId, id);

    const res = await queryClient.fetchQuery({
        gcTime: SONG_LIST_GC_MS,
        queryFn: async ({ signal }) =>
            api.controller.getPlaylistSongList({
                apiClientProps: {
                    serverId,
                    signal,
                },
                query: queryFilter,
            }),
        queryKey,
        staleTime: SONG_LIST_GC_MS,
    });

    if (res) {
        res.items = sortSongList(
            res.items,
            query?.sortBy || SongListSort.ID,
            query?.sortOrder || SortOrder.ASC,
        );
    }

    return res;
};

export const getAlbumSongsById = async (args: {
    id: string[];
    orderByIds?: boolean;
    query?: Partial<SongListQuery>;
    queryClient: QueryClient;
    serverId: string;
}) =>
    fetchSongList(args.queryClient, args.serverId, {
        albumIds: args.id,
        sortBy: SongListSort.ALBUM,
        sortOrder: SortOrder.ASC,
        startIndex: 0,
        ...args.query,
    });

export const getGenreSongsById = async (args: {
    id: string[];
    orderByIds?: boolean;
    query?: Partial<SongListQuery>;
    queryClient: QueryClient;
    serverId: string;
}) => {
    const { id, query, queryClient, serverId } = args;
    const data: SongListResponse = { items: [], startIndex: 0, totalRecordCount: 0 };

    for (const genreId of id) {
        const res = await fetchSongList(queryClient, serverId, {
            genreIds: [genreId],
            sortBy: SongListSort.GENRE,
            sortOrder: SortOrder.ASC,
            startIndex: 0,
            ...query,
        });

        data.items.push(...res!.items);
        if (data.totalRecordCount) {
            data.totalRecordCount += res!.totalRecordCount || 0;
        }
    }

    return data;
};

export const getAlbumArtistSongsById = async (args: {
    id: string[];
    orderByIds?: boolean;
    query?: Partial<SongListQuery>;
    queryClient: QueryClient;
    serverId: string;
}) =>
    fetchSongList(args.queryClient, args.serverId, {
        albumArtistIds: args.id || [],
        sortBy: SongListSort.ALBUM_ARTIST,
        sortOrder: SortOrder.ASC,
        startIndex: 0,
        ...args.query,
    });

export const getArtistSongsById = async (args: {
    id: string[];
    query?: Partial<SongListQuery>;
    queryClient: QueryClient;
    serverId: string;
}) =>
    fetchSongList(args.queryClient, args.serverId, {
        artistIds: args.id,
        sortBy: SongListSort.ALBUM,
        sortOrder: SortOrder.ASC,
        startIndex: 0,
        ...args.query,
    });

export const getSongsByQuery = async (args: {
    query?: Partial<SongListQuery>;
    queryClient: QueryClient;
    serverId: string;
}) =>
    fetchSongList(args.queryClient, args.serverId, {
        sortBy: SongListSort.ALBUM,
        sortOrder: SortOrder.ASC,
        startIndex: 0,
        ...args.query,
    });

export const getSongById = async (args: {
    id: string;
    queryClient: QueryClient;
    serverId: string;
}): Promise<SongListResponse> => {
    const { id, queryClient, serverId } = args;

    const queryFilter: SongDetailQuery = { id };

    const queryKey = queryKeys.songs.detail(serverId, queryFilter);

    const res = await queryClient.fetchQuery({
        gcTime: 1000 * 60,
        queryFn: async ({ signal }) =>
            api.controller.getSongDetail({
                apiClientProps: {
                    serverId,
                    signal,
                },
                query: queryFilter,
            }),
        queryKey,
        staleTime: 1000 * 60,
    });

    if (!res) throw new Error('Song not found');

    return {
        items: [res],
        startIndex: 0,
        totalRecordCount: 1,
    };
};

const getSongFieldValue = (song: Song, field: string): boolean | null | number | string => {
    switch (field) {
        case 'albumArtist':
            return song.albumArtists[0]?.name || '';
        case 'artist':
            return song.artistName || song.artists[0]?.name || '';
        case 'duration':
            return song.duration;
        case 'favorite':
            return song.userFavorite;
        case 'genre':
            return song.genres[0]?.name || '';
        case 'name':
            return song.name;
        case 'note':
            return song.comment || '';
        case 'path':
            return song.path || '';
        case 'playCount':
            return song.playCount;
        case 'year':
            return song.releaseYear || 0;
        default:
            return null;
    }
};

const matchesFilter = (song: Song, filter: PlayerFilter): boolean => {
    const songValue = getSongFieldValue(song, filter.field);
    const filterValue = filter.value;

    // Handle null/undefined values
    if (songValue === null || songValue === undefined) {
        return false;
    }

    switch (filter.operator) {
        case 'contains':
            return String(songValue).toLowerCase().includes(String(filterValue).toLowerCase());
        case 'endsWith':
            return String(songValue).toLowerCase().endsWith(String(filterValue).toLowerCase());
        case 'is':
            return String(songValue).toLowerCase() === String(filterValue).toLowerCase();
        case 'isNot':
            return String(songValue).toLowerCase() !== String(filterValue).toLowerCase();
        case 'lt':
            return Number(songValue) < Number(filterValue);
        case 'notContains':
            return !String(songValue).toLowerCase().includes(String(filterValue).toLowerCase());
        case 'regex': {
            try {
                const regex = new RegExp(String(filterValue), 'i');
                return regex.test(String(songValue));
            } catch {
                // Invalid regex pattern, don't match
                return false;
            }
        }
        case 'gt':
            return Number(songValue) > Number(filterValue);
        case 'startsWith':
            return String(songValue).toLowerCase().startsWith(String(filterValue).toLowerCase());
        default:
            return true;
    }
};

export const filterSongsByPlayerFilters = (songs: Song[], filters: PlayerFilter[]): Song[] => {
    // Filter out invalid filters (missing field, operator, or value)
    const validFilters = filters.filter(
        (filter) =>
            Boolean(filter.isEnabled) &&
            filter.field &&
            filter.operator &&
            filter.value !== undefined &&
            filter.value !== null &&
            filter.value !== '',
    );

    // If no valid filters, return all songs
    if (validFilters.length === 0) {
        return songs;
    }

    // Track filtered songs and their matching conditions
    const filteredSongs: Array<{ filter: PlayerFilter; song: Song }> = [];

    // Filter OUT songs that match any of the filters (exclude matching songs)
    const filtered = songs.filter((song) => {
        const matchingFilter = validFilters.find((filter) => matchesFilter(song, filter));
        if (matchingFilter) {
            filteredSongs.push({ filter: matchingFilter, song });
            return false;
        }
        return true;
    });

    if (filteredSongs.length > 0) {
        logFn.debug(logMsg[LogCategory.PLAYER].playerFiltersApplied, {
            category: LogCategory.PLAYER,
            meta: {
                filteredCount: filteredSongs.length,
                filteredSongs: filteredSongs.map(({ filter, song }) => ({
                    artist: song.artistName,
                    condition: {
                        field: filter.field,
                        operator: filter.operator,
                        value: filter.value,
                    },
                    songId: song.id,
                    songName: song.name,
                })),
                originalCount: songs.length,
                remainingCount: filtered.length,
            },
        });
    }

    return filtered;
};

export const getPlayerFiltersAndFilterSongs = (songs: Song[]): Song[] => {
    const state = useSettingsStore.getState();
    const filters = state.playback.filters;
    return filterSongsByPlayerFilters(songs, filters);
};