switched to using api tokens instead of login credentials

This commit is contained in:
Fishandchips321 2026-02-23 13:27:52 +00:00
parent 86f273d12d
commit 226caf4c1e
13 changed files with 215 additions and 85 deletions

View file

@ -0,0 +1,46 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ApiToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Owner")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Servers");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JellyGlassBackend.Migrations
{
/// <inheritdoc />
public partial class apiToken : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Password",
table: "Servers");
migrationBuilder.RenameColumn(
name: "Username",
table: "Servers",
newName: "ApiToken");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ApiToken",
table: "Servers",
newName: "Username");
migrationBuilder.AddColumn<string>(
name: "Password",
table: "Servers",
type: "TEXT",
nullable: false,
defaultValue: "");
}
}
}

View file

@ -21,22 +21,18 @@ namespace JellyGlassBackend.Migrations
b.Property<string>("Id") b.Property<string>("Id")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("ApiToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Owner") b.Property<string>("Owner")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Url") b.Property<string>("Url")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Servers"); b.ToTable("Servers");

View file

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

View file

@ -5,6 +5,5 @@ public class Server
public string Owner { get; set; } = string.Empty; public string Owner { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty; public string Url { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty; public string Id { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty; public string ApiToken { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
} }

View file

@ -10,20 +10,19 @@ namespace JellyGlass.Repositories;
public class JellyfinApiClient public class JellyfinApiClient
{ {
private string _apiKey = string.Empty; private readonly string _apiKey = string.Empty;
public readonly string InstanceUrl; public readonly string InstanceUrl;
private readonly HttpClient _client; private readonly HttpClient _client;
private readonly string _username, _password;
public string ID { get; private set; } = string.Empty; 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; InstanceUrl = instanceUrl;
_client = new HttpClient(); _client = new HttpClient();
_client.DefaultRequestHeaders.Clear(); _client.DefaultRequestHeaders.Clear();
_username = username; _apiKey = apiToken;
_password = password;
} }
public async Task<Item[]> GetInstanceLibraries() public async Task<Item[]> GetInstanceLibraries()
@ -37,7 +36,7 @@ public class JellyfinApiClient
var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>(); var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>();
return apiResponse.Items.ToArray(); return apiResponse!.Items.ToArray();
} }
catch (HttpRequestException e) catch (HttpRequestException e)
{ {
@ -47,76 +46,98 @@ public class JellyfinApiClient
public async Task<Item[]> GetItemChildren(string itemId) public async Task<Item[]> 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<ItemResponse>(); var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>();
return apiResponse!.Items.ToArray(); return apiResponse!.Items.ToArray();
}
catch (HttpRequestException e)
{
throw new JellyfinApiClientException(e.Message);
}
} }
public async Task<Item[]> 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 = "")
{ {
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<ItemResponse>(); var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>();
return apiResponse!.Items.ToArray(); return apiResponse!.Items.ToArray();
}
catch (HttpRequestException e)
{
throw new JellyfinApiClientException(e.Message);
}
} }
public async Task Authenticate() public async Task<ServerInfo> 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<ServerInfo>();
if (ID == string.Empty)
{ {
Username = _username, ID = apiResponse!.Id;
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<AuthResponse>();
_apiKey = authResponse!.AccessToken;
ID = authResponse.ServerId;
}
catch (HttpRequestException e)
{
//TODO: What to do on an exception
throw new JellyfinApiClientException(e.Message);
} }
return apiResponse!;
} }
public async Task<object> GetPublicServerInfo()
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{InstanceUrl}/System/Info/Public");
var response = await MakeRequest(request);
response.EnsureSuccessStatusCode();
var apiResponse = await response.Content.ReadFromJsonAsync<ServerInfo>();
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<AuthResponse>();
// _apiKey = authResponse!.AccessToken;
// ID = authResponse.ServerId;
// }
// catch (HttpRequestException e)
// {
// throw new JellyfinApiClientException(e.Message);
// }
// }
private async Task<HttpResponseMessage> MakeRequest(HttpRequestMessage request) private async Task<HttpResponseMessage> MakeRequest(HttpRequestMessage request)
{ {
request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader()); request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader());
@ -129,18 +150,19 @@ public class JellyfinApiClient
} }
catch (HttpRequestException e) catch (HttpRequestException e)
{ {
if (e.StatusCode == HttpStatusCode.Unauthorized) // if (e.StatusCode == HttpStatusCode.Unauthorized)
{ // {
await Authenticate(); // await Authenticate();
request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader()); // request.Headers.Authorization = new AuthenticationHeaderValue("MediaBrowser", GetAuthHeader());
response = await _client.SendAsync(request); // response = await _client.SendAsync(request);
} // }
else // else
{ // {
throw new JellyfinApiClientException(e.Message); // throw new JellyfinApiClientException(e.Message);
} // }
throw new JellyfinApiClientException(e.Message);
} }
return response; return response;

View file

@ -50,15 +50,26 @@ public class ClientService : IClientService
foreach (var server in servers) 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 try
{ {
await client.Authenticate(); await client.GetServerInfo(); //test the connection
} }
catch (JellyfinApiClientException e) catch (JellyfinApiClientException e)
{ {
_logger.LogError($"Error authenticating to {server.Url}. Error: {e.Message} Client will not be used");
continue;
} }
clients.Add(client); clients.Add(client);

View file

@ -9,11 +9,13 @@ public class ServerService : IServerService
{ {
private readonly IServerRepository _repository; private readonly IServerRepository _repository;
private readonly IClientService _service; private readonly IClientService _service;
private readonly ILogger<ServerService> _logger;
public ServerService(IServerRepository repository, IClientService service) public ServerService(IServerRepository repository, IClientService service, ILogger<ServerService> logger)
{ {
_repository = repository; _repository = repository;
_service = service; _service = service;
_logger = logger;
} }
public async Task<ServerDTO[]> GetServers() public async Task<ServerDTO[]> GetServers()
@ -24,6 +26,8 @@ public class ServerService : IServerService
foreach (var client in clients) foreach (var client in clients)
{ {
_logger.LogInformation($"ID for server {client.InstanceUrl} is {client.ID}");
var dto = new ServerDTO(); var dto = new ServerDTO();
var server = servers.First(s => s.Url == client.InstanceUrl); var server = servers.First(s => s.Url == client.InstanceUrl);
dto.Id = client.ID; dto.Id = client.ID;

View file

@ -9,12 +9,12 @@ const ServerList = () => {
getServerList().then(serverList => { getServerList().then(serverList => {
setServers(serverList); setServers(serverList);
}) })
}) }, []);
return ( return (
<div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }}> <div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }}>
{servers.map(server => { {servers.map(server => {
return (<ServerCard name={server.owner} online={false} linkTo={server.url} key={server.name} />) return (<ServerCard name={server.owner} online={true} linkTo={server.url} key={server.name} />)
})} })}
</div> </div>
) )

View file

@ -16,6 +16,7 @@ const ServerSearch = ({ searchTerm, server }: ServerSearchProps) => {
search(searchTerm, server.id).then(results => { search(searchTerm, server.id).then(results => {
setSearchResults(results); setSearchResults(results);
}).catch(err => { }).catch(err => {
setSearchResults([]);
alert(err); alert(err);
}) })
}, [searchTerm]); }, [searchTerm]);

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} - {searchResult.productionYear}</h3> <h3>{searchResult.type} - {searchResult.name} - {searchResult.productionYear}</h3>
</Link> </Link>
) )
} }

View file

@ -6,12 +6,10 @@ export interface SearchResult {
name: string; name: string;
id: string; id: string;
serverId: string; serverId: string;
type: SearchResultType; type: string;
productionYear: string; productionYear: string;
} }
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>> => {
const response = await axios.get<Array<SearchResult>>(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`); const response = await axios.get<Array<SearchResult>>(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`);

View file

@ -18,6 +18,11 @@ const Search = () => {
navigate("/"); navigate("/");
} }
if (servers.length > 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setServers([]);
}
getServerList().then(servers => { getServerList().then(servers => {
if (servers.length === 0) { if (servers.length === 0) {
alert("No servers found"); alert("No servers found");