diff --git a/backend/src/Migrations/20260223115822_apiToken.Designer.cs b/backend/src/Migrations/20260223115822_apiToken.Designer.cs new file mode 100644 index 0000000..5f15250 --- /dev/null +++ b/backend/src/Migrations/20260223115822_apiToken.Designer.cs @@ -0,0 +1,46 @@ +// +using JellyGlass.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace JellyGlassBackend.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260223115822_apiToken")] + partial class apiToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("JellyGlass.Models.Server", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Owner") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Servers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260223115822_apiToken.cs b/backend/src/Migrations/20260223115822_apiToken.cs new file mode 100644 index 0000000..1a49227 --- /dev/null +++ b/backend/src/Migrations/20260223115822_apiToken.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JellyGlassBackend.Migrations +{ + /// + public partial class apiToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Password", + table: "Servers"); + + migrationBuilder.RenameColumn( + name: "Username", + table: "Servers", + newName: "ApiToken"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "ApiToken", + table: "Servers", + newName: "Username"); + + migrationBuilder.AddColumn( + name: "Password", + table: "Servers", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/backend/src/Migrations/DatabaseContextModelSnapshot.cs b/backend/src/Migrations/DatabaseContextModelSnapshot.cs index 335317c..0594134 100644 --- a/backend/src/Migrations/DatabaseContextModelSnapshot.cs +++ b/backend/src/Migrations/DatabaseContextModelSnapshot.cs @@ -21,22 +21,18 @@ namespace JellyGlassBackend.Migrations b.Property("Id") .HasColumnType("TEXT"); + b.Property("ApiToken") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("Owner") .IsRequired() .HasColumnType("TEXT"); - b.Property("Password") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("Url") .IsRequired() .HasColumnType("TEXT"); - b.Property("Username") - .IsRequired() - .HasColumnType("TEXT"); - b.HasKey("Id"); b.ToTable("Servers"); diff --git a/backend/src/Models/JellyfinApi/ServerInfo.cs b/backend/src/Models/JellyfinApi/ServerInfo.cs new file mode 100644 index 0000000..466ee69 --- /dev/null +++ b/backend/src/Models/JellyfinApi/ServerInfo.cs @@ -0,0 +1,9 @@ +namespace JellyGlass.Models.JellyfinApi; + +public class ServerInfo +{ + public string ServerName { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; + public bool IsShuttingDown { get; set; } = false; +} \ No newline at end of file diff --git a/backend/src/Models/Server.cs b/backend/src/Models/Server.cs index 0b31f5f..e45a34e 100644 --- a/backend/src/Models/Server.cs +++ b/backend/src/Models/Server.cs @@ -5,6 +5,5 @@ public class Server public string Owner { get; set; } = string.Empty; public string Url { get; set; } = string.Empty; public string Id { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; - public string Username { get; set; } = string.Empty; + public string ApiToken { get; set; } = string.Empty; } \ No newline at end of file diff --git a/backend/src/Repositories/JellyfinApiClient.cs b/backend/src/Repositories/JellyfinApiClient.cs index 05abf4c..d2a35c4 100644 --- a/backend/src/Repositories/JellyfinApiClient.cs +++ b/backend/src/Repositories/JellyfinApiClient.cs @@ -10,20 +10,19 @@ namespace JellyGlass.Repositories; public class JellyfinApiClient { - private string _apiKey = string.Empty; + private readonly string _apiKey = string.Empty; public readonly string InstanceUrl; private readonly HttpClient _client; - private readonly string _username, _password; public string ID { get; private set; } = string.Empty; + public string ServerName { get; private set; } = string.Empty; - public JellyfinApiClient(string instanceUrl, string username, string password) + public JellyfinApiClient(string instanceUrl, string apiToken) { InstanceUrl = instanceUrl; _client = new HttpClient(); _client.DefaultRequestHeaders.Clear(); - _username = username; - _password = password; + _apiKey = apiToken; } public async Task GetInstanceLibraries() @@ -37,7 +36,7 @@ public class JellyfinApiClient var apiResponse = await response.Content.ReadFromJsonAsync(); - return apiResponse.Items.ToArray(); + return apiResponse!.Items.ToArray(); } catch (HttpRequestException e) { @@ -47,76 +46,98 @@ public class JellyfinApiClient public async Task GetItemChildren(string itemId) { - try - { - var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?ParentId={itemId}"); + var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/items?ParentId={itemId}"); - var response = await MakeRequest(request); + var response = await MakeRequest(request); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - var apiResponse = await response.Content.ReadFromJsonAsync(); + var apiResponse = await response.Content.ReadFromJsonAsync(); - return apiResponse!.Items.ToArray(); - } - catch (HttpRequestException e) - { - throw new JellyfinApiClientException(e.Message); - } + return apiResponse!.Items.ToArray(); } public async Task GetItems(string searchTerm = "", string years = "", string itemTypes = "", string limit = "", string parentId = "") { - try - { - 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=Series,Movie"); - var response = await MakeRequest(request); + var response = await MakeRequest(request); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - var apiResponse = await response.Content.ReadFromJsonAsync(); + var apiResponse = await response.Content.ReadFromJsonAsync(); - return apiResponse!.Items.ToArray(); - } - catch (HttpRequestException e) - { - throw new JellyfinApiClientException(e.Message); - } + return apiResponse!.Items.ToArray(); } - public async Task Authenticate() + public async Task GetServerInfo() { - var request = new HttpRequestMessage(HttpMethod.Post, $"{InstanceUrl}/Users/AuthenticateByName"); + var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/System/Info"); - request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader()); + var response = await MakeRequest(request); - var body = new + response.EnsureSuccessStatusCode(); + + var apiResponse = await response.Content.ReadFromJsonAsync(); + + if (ID == string.Empty) { - Username = _username, - Pw = _password - }; - - request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); - - try - { - var response = await _client.SendAsync(request); - - response.EnsureSuccessStatusCode(); - - var authResponse = await response.Content.ReadFromJsonAsync(); - - _apiKey = authResponse!.AccessToken; - ID = authResponse.ServerId; - } - catch (HttpRequestException e) - { - //TODO: What to do on an exception - throw new JellyfinApiClientException(e.Message); + ID = apiResponse!.Id; } + + return apiResponse!; } + public async Task GetPublicServerInfo() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/System/Info/Public"); + + var response = await MakeRequest(request); + + response.EnsureSuccessStatusCode(); + + var apiResponse = await response.Content.ReadFromJsonAsync(); + + if (ID == string.Empty) + { + ID = apiResponse!.Id; + } + + + return apiResponse!; + } + + // public async Task Authenticate() + // { + // var request = new HttpRequestMessage(HttpMethod.Post, $"{InstanceUrl}/Users/AuthenticateByName"); + + // request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader()); + + // var body = new + // { + // Username = _username, + // Pw = _password + // }; + + // request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + + // try + // { + // var response = await _client.SendAsync(request); + + // response.EnsureSuccessStatusCode(); + + // var authResponse = await response.Content.ReadFromJsonAsync(); + + // _apiKey = authResponse!.AccessToken; + // ID = authResponse.ServerId; + // } + // catch (HttpRequestException e) + // { + // throw new JellyfinApiClientException(e.Message); + // } + // } + private async Task MakeRequest(HttpRequestMessage request) { request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader()); @@ -129,18 +150,19 @@ public class JellyfinApiClient } catch (HttpRequestException e) { - if (e.StatusCode == HttpStatusCode.Unauthorized) - { - await Authenticate(); + // if (e.StatusCode == HttpStatusCode.Unauthorized) + // { + // await Authenticate(); - request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader()); + // request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader()); - response = await _client.SendAsync(request); - } - else - { - throw new JellyfinApiClientException(e.Message); - } + // response = await _client.SendAsync(request); + // } + // else + // { + // throw new JellyfinApiClientException(e.Message); + // } + throw new JellyfinApiClientException(e.Message); } return response; diff --git a/backend/src/Services/ClientService.cs b/backend/src/Services/ClientService.cs index 47e7407..9383517 100644 --- a/backend/src/Services/ClientService.cs +++ b/backend/src/Services/ClientService.cs @@ -50,15 +50,26 @@ public class ClientService : IClientService foreach (var server in servers) { - var client = new JellyfinApiClient(server.Url, server.Username, server.Password); + var client = new JellyfinApiClient(server.Url, server.ApiToken); + + // try + // { + // await client.Authenticate(); + // } + // catch (JellyfinApiClientException e) + // { + // _logger.LogError($"Error authenticating to {server.Url}. Error: {e.Message} Client will not be used"); + // continue; + // } try { - await client.Authenticate(); + await client.GetServerInfo(); //test the connection } catch (JellyfinApiClientException e) { - + _logger.LogError($"Error authenticating to {server.Url}. Error: {e.Message} Client will not be used"); + continue; } clients.Add(client); diff --git a/backend/src/Services/ServerService.cs b/backend/src/Services/ServerService.cs index 20b8581..5ad6775 100644 --- a/backend/src/Services/ServerService.cs +++ b/backend/src/Services/ServerService.cs @@ -9,11 +9,13 @@ public class ServerService : IServerService { private readonly IServerRepository _repository; private readonly IClientService _service; + private readonly ILogger _logger; - public ServerService(IServerRepository repository, IClientService service) + public ServerService(IServerRepository repository, IClientService service, ILogger logger) { _repository = repository; _service = service; + _logger = logger; } public async Task GetServers() @@ -24,6 +26,8 @@ public class ServerService : IServerService foreach (var client in clients) { + _logger.LogInformation($"ID for server {client.InstanceUrl} is {client.ID}"); + var dto = new ServerDTO(); var server = servers.First(s => s.Url == client.InstanceUrl); dto.Id = client.ID; diff --git a/frontend/src/Components/ServerList/ServerList.tsx b/frontend/src/Components/ServerList/ServerList.tsx index 56e4c2c..2b61520 100644 --- a/frontend/src/Components/ServerList/ServerList.tsx +++ b/frontend/src/Components/ServerList/ServerList.tsx @@ -9,12 +9,12 @@ const ServerList = () => { getServerList().then(serverList => { setServers(serverList); }) - }) + }, []); return (
{servers.map(server => { - return () + return () })}
) diff --git a/frontend/src/Components/ServerSearch/ServerSearch.tsx b/frontend/src/Components/ServerSearch/ServerSearch.tsx index 2dd7b67..cce7991 100644 --- a/frontend/src/Components/ServerSearch/ServerSearch.tsx +++ b/frontend/src/Components/ServerSearch/ServerSearch.tsx @@ -16,6 +16,7 @@ const ServerSearch = ({ searchTerm, server }: ServerSearchProps) => { search(searchTerm, server.id).then(results => { setSearchResults(results); }).catch(err => { + setSearchResults([]); alert(err); }) }, [searchTerm]); diff --git a/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx b/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx index 755aaab..e6882b8 100644 --- a/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx +++ b/frontend/src/Components/ServerSearch/ServerSearchResult/ServerSearchResult.tsx @@ -12,7 +12,7 @@ const ServerSearchResult = ({ searchResult, server }: ServerSearchResultProps) = return ( -

{searchResult.name} - {searchResult.productionYear}

+

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

) } diff --git a/frontend/src/Lib/Search.ts b/frontend/src/Lib/Search.ts index d0e3966..c18b01d 100644 --- a/frontend/src/Lib/Search.ts +++ b/frontend/src/Lib/Search.ts @@ -6,12 +6,10 @@ export interface SearchResult { name: string; id: string; serverId: string; - type: SearchResultType; + type: string; productionYear: string; } -export type SearchResultType = "movie" | "tv show" | "music"; - export const search = async (searchTerm: string, serverId: string): Promise> => { const response = await axios.get>(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`); diff --git a/frontend/src/Pages/Search/Search.tsx b/frontend/src/Pages/Search/Search.tsx index 8c7ad13..cafef8d 100644 --- a/frontend/src/Pages/Search/Search.tsx +++ b/frontend/src/Pages/Search/Search.tsx @@ -18,6 +18,11 @@ const Search = () => { navigate("/"); } + if (servers.length > 0) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setServers([]); + } + getServerList().then(servers => { if (servers.length === 0) { alert("No servers found");