Working search

This commit is contained in:
Fishandchips321 2026-02-22 21:58:23 +00:00
parent 271cf1f407
commit 2a572e8bc4
15 changed files with 217 additions and 39 deletions

View file

@ -1,3 +1,4 @@
using JellyGlass.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace JellyGlass.Controllers; namespace JellyGlass.Controllers;
@ -7,15 +8,19 @@ namespace JellyGlass.Controllers;
public class SearchController : ControllerBase public class SearchController : ControllerBase
{ {
private ILogger<SearchController> _logger; private ILogger<SearchController> _logger;
private ISearchService _service;
public SearchController(ILogger<SearchController> logger) public SearchController(ILogger<SearchController> logger, ISearchService service)
{ {
_logger = logger; _logger = logger;
_service = service;
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> handleSearch([FromQuery] string searchTerm) public async Task<IActionResult> handleSearch([FromQuery] string searchTerm, string serverId)
{ {
throw new NotImplementedException(); var results = await _service.Search(searchTerm, serverId);
return Ok(results);
} }
} }

View file

@ -16,6 +16,7 @@ public class ItemDTO
Index = item.IndexNumber; Index = item.IndexNumber;
ParentId = item.ParentId; ParentId = item.ParentId;
ThumbnailUrl = $"{this.ServerUrl}/Items/{ID}/Images/Primary"; ThumbnailUrl = $"{this.ServerUrl}/Items/{ID}/Images/Primary";
ProductionYear = item.ProductionYear.ToString();
} }
public string ID { get; set; } = string.Empty; public string ID { get; set; } = string.Empty;
@ -26,4 +27,5 @@ public class ItemDTO
public int? Index { get; set; } public int? Index { get; set; }
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; }
} }

View file

@ -27,6 +27,7 @@ builder.Services.AddTransient<ILibraryService, LibraryService>();
builder.Services.AddTransient<IServerRepository, ServerRepository>(); builder.Services.AddTransient<IServerRepository, ServerRepository>();
builder.Services.AddScoped<IClientService, ClientService>(); builder.Services.AddScoped<IClientService, ClientService>();
builder.Services.AddTransient<IServerService, ServerService>(); builder.Services.AddTransient<IServerService, ServerService>();
builder.Services.AddTransient<ISearchService, SearchService>();
var app = builder.Build(); var app = builder.Build();

View file

@ -5,4 +5,5 @@ namespace JellyGlass.Repositories;
public interface IServerRepository public interface IServerRepository
{ {
public Task<Server[]> GetServers(); public Task<Server[]> GetServers();
public Task<Server> GetServerById(string id);
} }

View file

@ -15,6 +15,8 @@ public class JellyfinApiClient
private readonly HttpClient _client; private readonly HttpClient _client;
private readonly string _username, _password; private readonly string _username, _password;
public string ID { get; private set; } = string.Empty;
public JellyfinApiClient(string instanceUrl, string username, string password) public JellyfinApiClient(string instanceUrl, string username, string password)
{ {
InstanceUrl = instanceUrl; InstanceUrl = instanceUrl;
@ -24,7 +26,7 @@ public class JellyfinApiClient
_password = password; _password = password;
} }
public async Task<ItemResponse> GetInstanceLibraries() public async Task<Item[]> GetInstanceLibraries()
{ {
try try
{ {
@ -35,7 +37,7 @@ public class JellyfinApiClient
var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>(); var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>();
return apiResponse!; return apiResponse.Items.ToArray();
} }
catch (HttpRequestException e) catch (HttpRequestException e)
{ {
@ -43,7 +45,7 @@ public class JellyfinApiClient
} }
} }
public async Task<ItemResponse> GetItemChildren(string itemId) public async Task<Item[]> GetItemChildren(string itemId)
{ {
try try
{ {
@ -55,7 +57,7 @@ public class JellyfinApiClient
var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>(); var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>();
return apiResponse!; return apiResponse!.Items.ToArray();
} }
catch (HttpRequestException e) catch (HttpRequestException e)
{ {
@ -63,16 +65,24 @@ public class JellyfinApiClient
} }
} }
public async Task<ItemResponse> GetItems(string searchTerm = "", string years = "", string itemTypes = "", string limit = "", string parentId = "") public async Task<Item[]> GetItems(string searchTerm = "", string years = "", string itemTypes = "", string limit = "", string parentId = "")
{ {
var query = new Dictionary<string, string>(); try
if (searchTerm != String.Empty)
{ {
query.Add("SearchTerm", searchTerm); var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?searchTerm={searchTerm}&recursive=true&includeItemTypes=Series,Movie");
}
throw new NotImplementedException(); var response = await MakeRequest(request);
response.EnsureSuccessStatusCode();
var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>();
return apiResponse!.Items.ToArray();
}
catch (HttpRequestException e)
{
throw new JellyfinApiClientException(e.Message);
}
} }
public async Task Authenticate() public async Task Authenticate()
@ -98,6 +108,7 @@ public class JellyfinApiClient
var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>(); var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>();
_apiKey = authResponse!.AccessToken; _apiKey = authResponse!.AccessToken;
ID = authResponse.ServerId;
} }
catch (HttpRequestException e) catch (HttpRequestException e)
{ {

View file

@ -18,4 +18,11 @@ public class ServerRepository : IServerRepository
return servers; return servers;
} }
public async Task<Server> GetServerById(string id)
{
var server = await _context.Servers.FirstOrDefaultAsync(s => s.Id == id);
return server;
}
} }

View file

@ -25,6 +25,24 @@ public class ClientService : IClientService
return _clients; return _clients;
} }
public async Task<JellyfinApiClient> GetClientForServerId(string serverId)
{
if (!_clients.Any())
{
await LoadClients();
}
foreach (var client in _clients)
{
if (client.ID == serverId)
{
return client;
}
}
throw new Exception($"Client with ID {serverId} not found");
}
private async Task LoadClients() private async Task LoadClients()
{ {
var servers = await _repository.GetServers(); var servers = await _repository.GetServers();

View file

@ -6,5 +6,5 @@ public interface IClientService
{ {
public Task<JellyfinApiClient[]> GetJellyfinClients(); public Task<JellyfinApiClient[]> GetJellyfinClients();
// public JellyfinApiClient GetClientForServer(string url); // public JellyfinApiClient GetClientForServer(string url);
// public JellyfinApiClient GetClientForServerId(string serverId); public Task<JellyfinApiClient> GetClientForServerId(string serverId);
} }

View file

@ -5,7 +5,9 @@ namespace JellyGlass.Services;
public interface ILibraryService public interface ILibraryService
{ {
public Task<Library[]> GetLibraries(); public Task<Library[]> GetLibraries();
public Task<ItemDTO[]> GetItemsFromLibrary(string libraryName); public Task<ItemDTO[]> GetItemsFromLibrary(string libraryName, string serverId);
public Task<Library[]> GetLibrariesFromServer(string serverId);
// public Task<ItemDTO[]> GetChildrenFromItems(ItemDTO[] items); // public Task<ItemDTO[]> GetChildrenFromItems(ItemDTO[] items);

View file

@ -0,0 +1,8 @@
using JellyGlass.Models;
namespace JellyGlass.Services;
public interface ISearchService
{
public Task<ItemDTO[]> Search(string searchTerm, string serverId);
}

View file

@ -1,19 +1,20 @@
using JellyGlass.Models; using JellyGlass.Models;
using JellyGlass.Models.JellyfinApi;
namespace JellyGlass.Services; namespace JellyGlass.Services;
public class LibraryService : ILibraryService public class LibraryService : ILibraryService
{ {
private IClientService _serverService; private IClientService _clientService;
public LibraryService(IClientService serverService) public LibraryService(IClientService serverService)
{ {
_serverService = serverService; _clientService = serverService;
} }
public async Task<Library[]> GetLibraries() public async Task<Library[]> GetLibraries()
{ {
var clients = await _serverService.GetJellyfinClients(); var clients = await _clientService.GetJellyfinClients();
var libraries = new Dictionary<string, Library>(); var libraries = new Dictionary<string, Library>();
@ -21,7 +22,7 @@ public class LibraryService : ILibraryService
{ {
var clientLibraries = await client.GetInstanceLibraries(); var clientLibraries = await client.GetInstanceLibraries();
foreach (var library in clientLibraries.Items) foreach (var library in clientLibraries)
{ {
if (library.Name == "Collections" || library.Name == "Playlists") if (library.Name == "Collections" || library.Name == "Playlists")
{ {
@ -43,9 +44,73 @@ public class LibraryService : ILibraryService
return libraries.Values.ToArray(); return libraries.Values.ToArray();
} }
public async Task<ItemDTO[]> GetItemsFromLibrary(string libraryName) public async Task<Item> GetLibrary(string libraryName, string serverId)
{ {
throw new NotImplementedException(); var client = await _clientService.GetClientForServerId(serverId);
if (client == null)
{
throw new Exception($"Could not find client with ID of {serverId}");
}
var libraries = await client.GetInstanceLibraries();
foreach (var library in libraries)
{
if (library.Name == libraryName)
{
return library;
}
}
throw new Exception("Couldn't find library");
}
public async Task<ItemDTO[]> GetItemsFromLibrary(string libraryName, string serverId)
{
var client = await _clientService.GetClientForServerId(serverId);
var library = await GetLibrary(libraryName, serverId);
var items = await client.GetItemChildren(library.Id);
var dtos = new List<ItemDTO>();
foreach (var item in items)
{
dtos.Add(new ItemDTO(item, client.InstanceUrl));
}
return dtos.ToArray();
}
public async Task<Library[]> GetLibrariesFromServer(string serverId)
{
var client = await _clientService.GetClientForServerId(serverId);
var libraries = new Dictionary<string, Library>();
var clientLibraries = await client.GetInstanceLibraries();
foreach (var library in clientLibraries)
{
if (library.Name == "Collections" || library.Name == "Playlists")
{
continue;
}
if (!libraries.ContainsKey(library.Name))
{
libraries.Add(library.Name, new Library()
{
Name = library.Name,
ThumbnailUrl = $"{client.InstanceUrl}/Items/{library.Id}/Primary"
});
}
}
return libraries.Values.ToArray();
} }
// public async Task<ItemDTO[]> GetChildrenFromItems(ItemDTO[] items) // public async Task<ItemDTO[]> GetChildrenFromItems(ItemDTO[] items)

View file

@ -0,0 +1,65 @@
using JellyGlass.Models;
using JellyGlass.Models.JellyfinApi;
namespace JellyGlass.Services;
public class SearchService : ISearchService
{
private ILibraryService _libraryService;
private IClientService _clientService;
public SearchService(ILibraryService libraryService, IClientService clientService)
{
_libraryService = libraryService;
_clientService = clientService;
}
public async Task<ItemDTO[]> Search(string searchTerm, string serverId)
{
var client = await _clientService.GetClientForServerId(serverId);
var items = await client.GetItems(searchTerm: searchTerm);
var dtos = new List<ItemDTO>();
foreach (var item in items)
{
dtos.Add(new ItemDTO(item, client.InstanceUrl));
}
return dtos.ToArray();
}
public async Task<ItemDTO[]> Search2(string searchTerm, string serverId)
{
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 serverId, Library library)
{
var items = await _libraryService.GetItemsFromLibrary(library.Name, serverId);
var foundItems = new List<ItemDTO>();
foreach (var item in items)
{
if (item.Name.Contains(searchTerm, StringComparison.CurrentCultureIgnoreCase))
{
foundItems.Add(item);
}
}
return foundItems.ToArray();
}
}

View file

@ -15,6 +15,8 @@ const ServerSearch = ({ searchTerm, server }: ServerSearchProps) => {
useEffect(() => { useEffect(() => {
search(searchTerm, server.id).then(results => { search(searchTerm, server.id).then(results => {
setSearchResults(results); setSearchResults(results);
}).catch(err => {
alert(err);
}) })
}, [searchTerm]); }, [searchTerm]);
@ -22,7 +24,7 @@ const ServerSearch = ({ searchTerm, server }: ServerSearchProps) => {
<Table striped bordered > <Table striped bordered >
<thead> <thead>
<tr> <tr>
<th>{server.name}</th> <th>{server.owner}'s server</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View file

@ -12,7 +12,7 @@ const ServerSearchResult = ({ searchResult, server }: ServerSearchResultProps) =
return ( return (
<Link to={resultUrl} target="_blank" rel="noopener noreferrer"> <Link to={resultUrl} target="_blank" rel="noopener noreferrer">
<h3>{searchResult.name}</h3> <h3>{searchResult.name} - {searchResult.productionYear}</h3>
</Link> </Link>
) )
} }

View file

@ -1,30 +1,21 @@
import axios from "axios";
import type { Server } from "./Servers"; import type { Server } from "./Servers";
import { apiUrl } from "./api";
export interface SearchResult { export interface SearchResult {
name: string; name: string;
id: string; id: string;
serverId: string; serverId: string;
type: SearchResultType; type: SearchResultType;
episodes: number; productionYear: string;
} }
export type SearchResultType = "movie" | "tv show" | "music"; export type SearchResultType = "movie" | "tv show" | "music";
export const search = async (searchTerm: string, serverId: string): Promise<Array<SearchResult>> => { export const search = async (searchTerm: string, serverId: string): Promise<Array<SearchResult>> => {
return [{ const response = await axios.get<Array<SearchResult>>(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`);
name: "Test Result",
episodes: 0, return response.data;
id: "awawa",
serverId: serverId,
type: "movie"
},
{
name: "Test result 2",
episodes: 0,
id: "awawa 2",
serverId: serverId,
type: "movie"
}];
} }
export const getUrlForSearchResult = (result: SearchResult, server: Server): string => { export const getUrlForSearchResult = (result: SearchResult, server: Server): string => {