Compare commits

..

5 commits
v1.0 ... main

10 changed files with 134 additions and 37 deletions

View file

@ -28,4 +28,6 @@ public class ItemDTO
public string? ParentId { get; set; } public string? ParentId { get; set; }
public string? ThumbnailUrl { get; set; } public string? ThumbnailUrl { get; set; }
public string? ProductionYear { get; set; } public string? ProductionYear { get; set; }
public string[]? SubtitleLanguages { get; set; }
public string[]? AudioLanguages { get; set; }
} }

View file

@ -21,4 +21,6 @@ public class Item
public string Type { get; set; } = string.Empty; public string Type { get; set; } = string.Empty;
public double PrimaryImageAspectRatio { get; set; } public double PrimaryImageAspectRatio { get; set; }
public string? CollectionType { get; set; } public string? CollectionType { get; set; }
public bool? HasSubtitles { get; set; }
public List<MediaStream>? MediaStreams { get; set; }
} }

View file

@ -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";
}

View file

@ -46,9 +46,31 @@ public class JellyfinApiClient
return apiResponse!.Items.ToArray(); return apiResponse!.Items.ToArray();
} }
public async Task<Item[]> GetItems(string searchTerm = "", string years = "", string itemTypes = "", string limit = "", string parentId = "") public async Task<Item[]> 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<ItemResponse>();
return apiResponse!.Items.ToArray();
}
public async Task<Item[]> 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<ItemResponse>();
return apiResponse!.Items.ToArray();
}
public async Task<Item[]> GetMediaInfoFromMovie(string movieId)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?ids={movieId}&fields=MediaStreams");
var response = await MakeRequest(request); var response = await MakeRequest(request);

View file

@ -43,6 +43,23 @@ public class ClientService : IClientService
return client; return client;
} }
public async Task<JellyfinApiClient> 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) public async Task LoadNewClient(Server server)
{ {
var client = new JellyfinApiClient(server.Url, server.ApiToken); var client = new JellyfinApiClient(server.Url, server.ApiToken);

View file

@ -7,6 +7,7 @@ public interface IClientService
{ {
public Task<JellyfinApiClient[]> GetClients(); public Task<JellyfinApiClient[]> GetClients();
public Task<JellyfinApiClient> GetClientFromUrl(string url); public Task<JellyfinApiClient> GetClientFromUrl(string url);
public Task<JellyfinApiClient> GetClientFromJellyfinServerID(string jellyfinServerID); //the ID of the jellyfin server, not the server stored in our db
public Task LoadNewClient(Server server); public Task LoadNewClient(Server server);
public void UnloadClient(JellyfinApiClient client); public void UnloadClient(JellyfinApiClient client);
} }

View file

@ -5,12 +5,12 @@ namespace JellyGlass.Services;
public class SearchService : ISearchService public class SearchService : ISearchService
{ {
private ILibraryService _libraryService; private ILogger<SearchService> _logger;
private IClientService _clientService; private IClientService _clientService;
public SearchService(ILibraryService libraryService, IClientService clientService) public SearchService(ILogger<SearchService> logger, IClientService clientService)
{ {
_libraryService = libraryService; _logger = logger;
_clientService = clientService; _clientService = clientService;
} }
@ -18,48 +18,60 @@ public class SearchService : ISearchService
{ {
var client = await _clientService.GetClientFromUrl(serverUrl); var client = await _clientService.GetClientFromUrl(serverUrl);
var items = await client.GetItems(searchTerm: searchTerm); var items = await client.Search(searchTerm: searchTerm);
var dtos = new List<ItemDTO>(); var dtos = new List<ItemDTO>();
foreach (var item in items) 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(); return dtos.ToArray();
} }
// public async Task<ItemDTO[]> Search2(string searchTerm, int serverId) private async Task<Tuple<string[], string[]>> GetLanguagesForItem(Item item)
// {
// var libraries = await _libraryService.GetLibrariesFromServer(serverId);
// var foundItems = new List<ItemDTO>();
// foreach (var library in libraries)
// {
// var found = await SearchLibraryForTerm(searchTerm, serverId, library);
// foundItems.AddRange(found);
// }
// return foundItems.ToArray();
// }
private async Task<ItemDTO[]> SearchLibraryForTerm(string searchTerm, string serverUrl, Library library)
{ {
var items = await _libraryService.GetItemsFromLibrary(library.Name, serverUrl); var subtitleLanguages = new List<string>();
var audioLanguages = new List<string>();
var client = await _clientService.GetClientFromJellyfinServerID(item.ServerId);
var foundItems = new List<ItemDTO>(); Item[] infoList;
foreach (var item in items) 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<string[], string[]>(audioLanguages.ToArray(), subtitleLanguages.ToArray());
} }
} }

View file

@ -10,10 +10,24 @@ interface ServerSearchResultProps {
const ServerSearchResult = ({ searchResult, server }: ServerSearchResultProps) => { const ServerSearchResult = ({ searchResult, server }: ServerSearchResultProps) => {
const resultUrl = getUrlForSearchResult(searchResult, server); const resultUrl = getUrlForSearchResult(searchResult, server);
function getLangs(languages: Array<string>) {
let formattedLangs = languages[0];
for (let i = 1; i < languages.length; i++) {
formattedLangs += `, ${languages[i]}`;
}
return formattedLangs;
}
return ( return (
<Link to={resultUrl} target="_blank" rel="noopener noreferrer"> <>
<h3>{searchResult.type} - {searchResult.name} - {searchResult.productionYear}</h3> <Link to={resultUrl} target="_blank" rel="noopener noreferrer">
</Link> <h3>{searchResult.type} - {searchResult.name} - {searchResult.productionYear}</h3>
</Link>
{searchResult.subtitleLanguages.length > 0 && <p>Audio Languages: {getLangs(searchResult.audioLanguages)}</p>}
{searchResult.subtitleLanguages.length > 0 && <p>Subtitle Languages: {getLangs(searchResult.subtitleLanguages)}</p>}
</>
) )
} }

View file

@ -8,6 +8,8 @@ export interface SearchResult {
serverId: string; serverId: string;
type: string; type: string;
productionYear: string; productionYear: string;
subtitleLanguages: Array<string>;
audioLanguages: Array<string>;
} }
export const search = async (searchTerm: string, serverUrl: string): Promise<Array<SearchResult>> => { export const search = async (searchTerm: string, serverUrl: string): Promise<Array<SearchResult>> => {

View file

@ -1,20 +1,35 @@
import { useEffect, useState } from "react"; import { useEffect, useReducer } from "react";
import { getServerList, type Server } from "../../Lib/Servers"; import { getServerList, type Server } from "../../Lib/Servers";
import ServerSearch from "../../Components/ServerSearch/ServerSearch"; import ServerSearch from "../../Components/ServerSearch/ServerSearch";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { Spinner } from "react-bootstrap"; import { Spinner } from "react-bootstrap";
import Cookies from "js-cookie"; 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 Search = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [servers, setServers] = useState<Array<Server>>([]); // const [servers, setServers] = useState<Array<Server>>([]);
const [servers, serversDispatch] = useReducer(serverReducer, []);
const navigate = useNavigate(); const navigate = useNavigate();
const sessionCookie = Cookies.get("session"); const sessionCookie = Cookies.get("session");
const searchTerm = searchParams.get("search") || ""; const searchTerm = searchParams.get("search") || "";
useEffect(() => { useEffect(() => {
serversDispatch({ type: "CLEAR" });
if (!sessionCookie) { if (!sessionCookie) {
navigate("/login"); navigate("/login");
return; return;
@ -43,11 +58,11 @@ const Search = () => {
navigate("/"); navigate("/");
} }
setServers(workingServers); serversDispatch({ type: "SET", payload: workingServers });
}).catch(e => { }).catch(e => {
alert(e); alert(e);
}); });
}, [searchTerm, navigate]); }, [searchTerm, navigate, sessionCookie]);
return ( return (
<> <>