From 95e5efa533940895cd0bd2bc40de796d2807c154 Mon Sep 17 00:00:00 2001 From: Fishandchips321 Date: Mon, 9 Mar 2026 18:19:09 +0000 Subject: [PATCH 1/4] added audio and subtitle language info to search results --- backend/src/Models/ItemDTO.cs | 2 + backend/src/Models/JellyfinApi/Item.cs | 2 + backend/src/Models/JellyfinApi/MediaStream.cs | 10 +++ backend/src/Repositories/JellyfinApiClient.cs | 26 ++++++- backend/src/Services/ClientService.cs | 17 +++++ backend/src/Services/IClientService.cs | 1 + backend/src/Services/SearchService.cs | 70 +++++++++++-------- 7 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 backend/src/Models/JellyfinApi/MediaStream.cs diff --git a/backend/src/Models/ItemDTO.cs b/backend/src/Models/ItemDTO.cs index b9e26d6..a339783 100644 --- a/backend/src/Models/ItemDTO.cs +++ b/backend/src/Models/ItemDTO.cs @@ -28,4 +28,6 @@ public class ItemDTO public string? ParentId { get; set; } public string? ThumbnailUrl { get; set; } public string? ProductionYear { get; set; } + public string[]? SubtitleLanguages { get; set; } + public string[]? AudioLanguages { get; set; } } \ No newline at end of file diff --git a/backend/src/Models/JellyfinApi/Item.cs b/backend/src/Models/JellyfinApi/Item.cs index 9d0bc38..ab15b28 100644 --- a/backend/src/Models/JellyfinApi/Item.cs +++ b/backend/src/Models/JellyfinApi/Item.cs @@ -21,4 +21,6 @@ public class Item public string Type { get; set; } = string.Empty; public double PrimaryImageAspectRatio { get; set; } public string? CollectionType { get; set; } + public bool? HasSubtitles { get; set; } + public List? MediaStreams { get; set; } } \ No newline at end of file diff --git a/backend/src/Models/JellyfinApi/MediaStream.cs b/backend/src/Models/JellyfinApi/MediaStream.cs new file mode 100644 index 0000000..376f217 --- /dev/null +++ b/backend/src/Models/JellyfinApi/MediaStream.cs @@ -0,0 +1,10 @@ +namespace JellyGlass.Models.JellyfinApi; + +public class MediaStream +{ + public string Title { get; set; } + public string DisplayTitle { get; set; } + public string Type { get; set; } + public string? LocalizedDefault { get; set; } + public string Language { get; set; } = "undefined"; +} \ No newline at end of file diff --git a/backend/src/Repositories/JellyfinApiClient.cs b/backend/src/Repositories/JellyfinApiClient.cs index 4132ffc..acd6d85 100644 --- a/backend/src/Repositories/JellyfinApiClient.cs +++ b/backend/src/Repositories/JellyfinApiClient.cs @@ -46,9 +46,31 @@ public class JellyfinApiClient return apiResponse!.Items.ToArray(); } - public async Task GetItems(string searchTerm = "", string years = "", string itemTypes = "", string limit = "", string parentId = "") + public async Task Search(string searchTerm = "", string itemTypes = "Series,Movie", string limit = "") { - var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?searchTerm={searchTerm}&recursive=true&includeItemTypes=Series,Movie"); + var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?searchTerm={searchTerm}&recursive=true&includeItemTypes={itemTypes}&limit={limit}"); + + var response = await MakeRequest(request); + + var apiResponse = await response.Content.ReadFromJsonAsync(); + + return apiResponse!.Items.ToArray(); + } + + public async Task GetMediaInfoFromTvSeries(string seriesId) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?parentId={seriesId}&recursive=true&includeItemTypes=Episode&fields=MediaStreams"); + + var response = await MakeRequest(request); + + var apiResponse = await response.Content.ReadFromJsonAsync(); + + return apiResponse!.Items.ToArray(); + } + + public async Task GetMediaInfoFromMovie(string movieId) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?ids={movieId}&fields=MediaStreams"); var response = await MakeRequest(request); diff --git a/backend/src/Services/ClientService.cs b/backend/src/Services/ClientService.cs index 23d1cc4..6f00aec 100644 --- a/backend/src/Services/ClientService.cs +++ b/backend/src/Services/ClientService.cs @@ -43,6 +43,23 @@ public class ClientService : IClientService return client; } + public async Task GetClientFromJellyfinServerID(string jellyfinServerID) + { + if (!_clients.Any()) + { + await LoadClients(); + } + + var client = _clients.FirstOrDefault(c => c.ID == jellyfinServerID); + + if (client == null) + { + throw new Exception($"Could not find a client with ID of {jellyfinServerID}"); + } + + return client; + } + public async Task LoadNewClient(Server server) { var client = new JellyfinApiClient(server.Url, server.ApiToken); diff --git a/backend/src/Services/IClientService.cs b/backend/src/Services/IClientService.cs index f4d3191..b1edec7 100644 --- a/backend/src/Services/IClientService.cs +++ b/backend/src/Services/IClientService.cs @@ -7,6 +7,7 @@ public interface IClientService { public Task GetClients(); public Task GetClientFromUrl(string url); + public Task GetClientFromJellyfinServerID(string jellyfinServerID); //the ID of the jellyfin server, not the server stored in our db public Task LoadNewClient(Server server); public void UnloadClient(JellyfinApiClient client); } \ No newline at end of file diff --git a/backend/src/Services/SearchService.cs b/backend/src/Services/SearchService.cs index 2632e56..49d74ad 100644 --- a/backend/src/Services/SearchService.cs +++ b/backend/src/Services/SearchService.cs @@ -5,12 +5,12 @@ namespace JellyGlass.Services; public class SearchService : ISearchService { - private ILibraryService _libraryService; + private ILogger _logger; private IClientService _clientService; - public SearchService(ILibraryService libraryService, IClientService clientService) + public SearchService(ILogger logger, IClientService clientService) { - _libraryService = libraryService; + _logger = logger; _clientService = clientService; } @@ -18,48 +18,62 @@ public class SearchService : ISearchService { var client = await _clientService.GetClientFromUrl(serverUrl); - var items = await client.GetItems(searchTerm: searchTerm); + var items = await client.Search(searchTerm: searchTerm); var dtos = new List(); foreach (var item in items) { - dtos.Add(new ItemDTO(item, client.InstanceUrl)); + var dto = new ItemDTO(item, client.InstanceUrl); + var languages = await GetLanguagesForItem(item); + dto.SubtitleLanguages = languages.Item2; + dto.AudioLanguages = languages.Item1; + dtos.Add(dto); } return dtos.ToArray(); } - // public async Task Search2(string searchTerm, int serverId) - // { - // var libraries = await _libraryService.GetLibrariesFromServer(serverId); - - // var foundItems = new List(); - - // foreach (var library in libraries) - // { - // var found = await SearchLibraryForTerm(searchTerm, serverId, library); - - // foundItems.AddRange(found); - // } - - // return foundItems.ToArray(); - // } - - private async Task SearchLibraryForTerm(string searchTerm, string serverUrl, Library library) + private async Task> GetLanguagesForItem(Item item) { - var items = await _libraryService.GetItemsFromLibrary(library.Name, serverUrl); + _logger.LogInformation($"Getting subtitle languages for {item.Name}"); - var foundItems = new List(); + var subtitleLanguages = new List(); + var audioLanguages = new List(); + var client = await _clientService.GetClientFromJellyfinServerID(item.ServerId); - foreach (var item in items) + Item[] infoList; + + if (item.Type == "Movie") { - if (item.Name.Contains(searchTerm, StringComparison.CurrentCultureIgnoreCase)) + infoList = await client.GetMediaInfoFromMovie(item.Id); + } + else if (item.Type == "Series") + { + infoList = await client.GetMediaInfoFromTvSeries(item.Id); + } + else + { + throw new Exception(); + } + + _logger.LogInformation($"Found {infoList.Count()} items"); + + foreach (var info in infoList) + { + foreach (var mediaStream in info.MediaStreams!) { - foundItems.Add(item); + if (mediaStream.Type == "Subtitle" && !subtitleLanguages.Contains(mediaStream.Language)) + { + subtitleLanguages.Add(mediaStream.Language); + } + else if (mediaStream.Type == "Audio" && !audioLanguages.Contains(mediaStream.Language)) + { + audioLanguages.Add(mediaStream.Language); + } } } - return foundItems.ToArray(); + return new Tuple(audioLanguages.ToArray(), subtitleLanguages.ToArray()); } } \ No newline at end of file From 17fc623179e5f1edd2e4da42277a938e5b4a7308 Mon Sep 17 00:00:00 2001 From: Fishandchips321 Date: Tue, 10 Mar 2026 16:23:43 +0000 Subject: [PATCH 2/4] added language info to search results --- .../ServerSearchResult/ServerSearchResult.tsx | 20 ++++++++++++++++--- frontend/src/Lib/Search.ts | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx b/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx index e6882b8..6f378f6 100644 --- a/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx +++ b/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx @@ -10,10 +10,24 @@ interface ServerSearchResultProps { const ServerSearchResult = ({ searchResult, server }: ServerSearchResultProps) => { const resultUrl = getUrlForSearchResult(searchResult, server); + function getLangs(languages: Array) { + let formattedLangs = languages[0]; + + for (let i = 1; i < languages.length; i++) { + formattedLangs += `, ${languages[i]}`; + } + + return formattedLangs; + } + return ( - -

{searchResult.type} - {searchResult.name} - {searchResult.productionYear}

- + <> + +

{searchResult.type} - {searchResult.name} - {searchResult.productionYear}

+ + {searchResult.subtitleLanguages.length > 0 &&

Audio Languages: {getLangs(searchResult.audioLanguages)}

} + {searchResult.subtitleLanguages.length > 0 &&

Subtitle Languages: {getLangs(searchResult.subtitleLanguages)}

} + ) } diff --git a/frontend/src/Lib/Search.ts b/frontend/src/Lib/Search.ts index 2649ddd..89b17d1 100644 --- a/frontend/src/Lib/Search.ts +++ b/frontend/src/Lib/Search.ts @@ -8,6 +8,8 @@ export interface SearchResult { serverId: string; type: string; productionYear: string; + subtitleLanguages: Array; + audioLanguages: Array; } export const search = async (searchTerm: string, serverUrl: string): Promise> => { From 9d81b0a223ab9c83627067367ff88201059fdafd Mon Sep 17 00:00:00 2001 From: Fishandchips321 Date: Tue, 10 Mar 2026 16:24:05 +0000 Subject: [PATCH 3/4] fixed search results not getting cleared when doing another search --- frontend/src/Pages/Search/Search.tsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/frontend/src/Pages/Search/Search.tsx b/frontend/src/Pages/Search/Search.tsx index 33d8c07..256bbf3 100644 --- a/frontend/src/Pages/Search/Search.tsx +++ b/frontend/src/Pages/Search/Search.tsx @@ -1,20 +1,35 @@ -import { useEffect, useState } from "react"; +import { useEffect, useReducer } from "react"; import { getServerList, type Server } from "../../Lib/Servers"; import ServerSearch from "../../Components/ServerSearch/ServerSearch"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Spinner } from "react-bootstrap"; import Cookies from "js-cookie"; +type serverListAction = { type: "CLEAR" } | { type: "SET", payload: Server[] }; + +const serverReducer = (state: Server[], action: serverListAction): Server[] => { + switch (action.type) { + case "CLEAR": + return []; + case "SET": + return action.payload; + default: + return state; + } +} const Search = () => { const [searchParams] = useSearchParams(); - const [servers, setServers] = useState>([]); + // const [servers, setServers] = useState>([]); + const [servers, serversDispatch] = useReducer(serverReducer, []); const navigate = useNavigate(); const sessionCookie = Cookies.get("session"); const searchTerm = searchParams.get("search") || ""; useEffect(() => { + serversDispatch({ type: "CLEAR" }); + if (!sessionCookie) { navigate("/login"); return; @@ -43,11 +58,11 @@ const Search = () => { navigate("/"); } - setServers(workingServers); + serversDispatch({ type: "SET", payload: workingServers }); }).catch(e => { alert(e); }); - }, [searchTerm, navigate]); + }, [searchTerm, navigate, sessionCookie]); return ( <> From f19c23a7bbb7b61824a533e9d803394dd09c9ea0 Mon Sep 17 00:00:00 2001 From: Fishandchips321 Date: Tue, 10 Mar 2026 16:25:01 +0000 Subject: [PATCH 4/4] removed unneeded logging --- backend/src/Services/SearchService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/Services/SearchService.cs b/backend/src/Services/SearchService.cs index 49d74ad..04af3e8 100644 --- a/backend/src/Services/SearchService.cs +++ b/backend/src/Services/SearchService.cs @@ -36,8 +36,6 @@ public class SearchService : ISearchService private async Task> GetLanguagesForItem(Item item) { - _logger.LogInformation($"Getting subtitle languages for {item.Name}"); - var subtitleLanguages = new List(); var audioLanguages = new List(); var client = await _clientService.GetClientFromJellyfinServerID(item.ServerId);