switched to using api tokens instead of login credentials
This commit is contained in:
parent
86f273d12d
commit
226caf4c1e
13 changed files with 215 additions and 85 deletions
46
backend/src/Migrations/20260223115822_apiToken.Designer.cs
generated
Normal file
46
backend/src/Migrations/20260223115822_apiToken.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
backend/src/Migrations/20260223115822_apiToken.cs
Normal file
39
backend/src/Migrations/20260223115822_apiToken.cs
Normal 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: "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,22 +21,18 @@ namespace JellyGlassBackend.Migrations
|
|||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApiToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Servers");
|
||||
|
|
|
|||
9
backend/src/Models/JellyfinApi/ServerInfo.cs
Normal file
9
backend/src/Models/JellyfinApi/ServerInfo.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<Item[]> GetInstanceLibraries()
|
||||
|
|
@ -37,7 +36,7 @@ public class JellyfinApiClient
|
|||
|
||||
var apiResponse = await response.Content.ReadFromJsonAsync<ItemResponse>();
|
||||
|
||||
return apiResponse.Items.ToArray();
|
||||
return apiResponse!.Items.ToArray();
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
|
|
@ -47,76 +46,98 @@ public class JellyfinApiClient
|
|||
|
||||
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();
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
throw new JellyfinApiClientException(e.Message);
|
||||
}
|
||||
return apiResponse!.Items.ToArray();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
throw new JellyfinApiClientException(e.Message);
|
||||
}
|
||||
return apiResponse!.Items.ToArray();
|
||||
}
|
||||
|
||||
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,
|
||||
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);
|
||||
ID = apiResponse!.Id;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ public class ServerService : IServerService
|
|||
{
|
||||
private readonly IServerRepository _repository;
|
||||
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;
|
||||
_service = service;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ServerDTO[]> 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;
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ const ServerList = () => {
|
|||
getServerList().then(serverList => {
|
||||
setServers(serverList);
|
||||
})
|
||||
})
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }}>
|
||||
{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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const ServerSearch = ({ searchTerm, server }: ServerSearchProps) => {
|
|||
search(searchTerm, server.id).then(results => {
|
||||
setSearchResults(results);
|
||||
}).catch(err => {
|
||||
setSearchResults([]);
|
||||
alert(err);
|
||||
})
|
||||
}, [searchTerm]);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const ServerSearchResult = ({ searchResult, server }: ServerSearchResultProps) =
|
|||
|
||||
return (
|
||||
<Link to={resultUrl} target="_blank" rel="noopener noreferrer">
|
||||
<h3>{searchResult.name} - {searchResult.productionYear}</h3>
|
||||
<h3>{searchResult.type} - {searchResult.name} - {searchResult.productionYear}</h3>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Array<SearchResult>> => {
|
||||
const response = await axios.get<Array<SearchResult>>(`${apiUrl}/search?searchTerm=${searchTerm}&serverId=${serverId}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue