Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 976718b259 | |||
| f19c23a7bb | |||
| 9d81b0a223 | |||
| 17fc623179 | |||
| 95e5efa533 |
10 changed files with 134 additions and 37 deletions
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
10
backend/src/Models/JellyfinApi/MediaStream.cs
Normal file
10
backend/src/Models/JellyfinApi/MediaStream.cs
Normal 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";
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>> => {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue