Basic search UI (no download links yet)

This commit is contained in:
2024-11-09 17:13:02 +01:00
parent 804e5e91a0
commit bfb76389cf
35 changed files with 5502 additions and 4 deletions

View File

@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JellyfinCinemaStream", "JellyfinCinemaStream.csproj", "{7DE997A9-52B8-41E4-9958-1115BA3E481D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamCinemaLib", "StreamCinemaLib\StreamCinemaLib.csproj", "{7DE997A9-52B8-41E4-9958-1115BA3E481D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamCinemaWeb", "StreamCinemaWeb\StreamCinemaWeb.csproj", "{6B2550AF-200C-40B3-95AE-892A604B9A76}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +17,10 @@ Global
{7DE997A9-52B8-41E4-9958-1115BA3E481D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DE997A9-52B8-41E4-9958-1115BA3E481D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DE997A9-52B8-41E4-9958-1115BA3E481D}.Release|Any CPU.Build.0 = Release|Any CPU
{6B2550AF-200C-40B3-95AE-892A604B9A76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B2550AF-200C-40B3-95AE-892A604B9A76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B2550AF-200C-40B3-95AE-892A604B9A76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B2550AF-200C-40B3-95AE-892A604B9A76}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class Art
{
public string poster {get;set;} // imge url
public string fanart {get;set;} // image url
}

View File

@@ -0,0 +1,11 @@
using System;
namespace StreamCinemaLib.API;
public class Cast
{
public string name { get; set; } // Person's name ie. "Josef Maršálek",
public string role { get; set; } // ie. "Judge",
public string thumbnail { get; set; } // thumb URL ie. https://image.tmdb.org/t/p/original//iI5zJZUG2SQvkEtuZDtJD0gYt2p.jpg
public int order { get; set; }
}

View File

@@ -0,0 +1,10 @@
using System;
namespace StreamCinemaLib.API;
public class FilterHits
{
public FilterTotalHits? total {get;set;}
public double max_score {get;set;}
public List<MediaInfo>? hits {get; set;}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace StreamCinemaLib.API;
public class FilterResponse
{
public int took { get; set; }
public bool timed_out { get; set; }
public ShardsInfo? _shards { get; set; }
public FilterHits? hits { get; set; }
}

View File

@@ -0,0 +1,20 @@
namespace StreamCinemaLib.API;
public enum FilterSortBy
{
Score,
Year,
Premiered,
DateAdded,
LastChildrenDateAdded,
LastChildPremiered,
Title,
PlayCount,
LastSeen,
Episode,
News,
Popularity,
Trending,
LangDateAdded,
Custom,
}

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class FilterTotalHits
{
public double value { get; set; }
public string relation { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System;
namespace StreamCinemaLib.API;
public class InfoLabelI18n
{
public string lang { get; set; } // two letter iso code ie. "cs",
public string title { get; set; }
public string? plot { get; set; }
public string? tagline { get; set; }
public Art? art { get; set; }
}

View File

@@ -0,0 +1,20 @@
using System;
namespace StreamCinemaLib.API;
public class InfoLabels
{
public string? originaltitle { get; set; }
public List<string>? genre { get; set; } // ie. "Reality","Competitive", "Reality-TV", "Food", "Game Show"
public int year { get; set; }
public List<string>? director { get; set; } // people names like "David Slabý"
public List<string>? studio { get; set; } // people names like "Česká televize"
public List<string>? writer { get; set; } // people names like "Michaela Hronová"
public DateTime? premiered { get; set; }
public DateTime? aired { get; set; }
public DateTime? dateadded { get; set; }
public string? mediatype { get; set; }
public List<string>? country { get; set; }// ie. "CZ", "Czech Republic"
public string? status { get; set; } // ie. "Returning Series"
public double? duration { get; set; } // in seconds
}

View File

@@ -0,0 +1,7 @@
namespace StreamCinemaLib.API;
public enum ItemOrder
{
Ascending,
Descending
}

View File

@@ -0,0 +1,12 @@
namespace StreamCinemaLib.API;
public enum ItemType
{
TVShow,
Concert,
Anime,
Movie,
Season,
Episode,
All,
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Text.Json.Serialization;
namespace StreamCinemaLib.API;
public class MediaFormatInfo
{
public bool hdr {get;set;}
public bool dv {get;set;}
[JsonPropertyName("3d")]
public bool has3d {get;set;}
public List<string>? hdr_formats {get; set;}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace StreamCinemaLib.API;
public class MediaInfo
{
public string? _index { get; set; }
public string? _id { get; set; }
public double _score { get; set; }
//"_ignored": [
// "i18n_info_labels.plot.keyword"
//],
public MediaSource? _source { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class MediaLanguageInfo
{
public List<MediaLanguageItem>? items { get; set; }
public List<string>? map { get; set; } // two letter ISO codes
}

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class MediaLanguageInfos
{
public MediaLanguageInfo? audio { get; set; }
public MediaLanguageInfo? subtitles { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class MediaLanguageItem
{
public string lang { get; set; } // two letter ISO code
public DateTime? date_added { get; set; } // ie. 2022-05-01T21:29:24.804Z
}

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class MediaRating
{
public double rating { get; set; }
public int votes { get; set; }
}

View File

@@ -0,0 +1,10 @@
using System;
namespace StreamCinemaLib.API;
public class MediaRatings
{
public MediaRating? tmdb { get; set; }
public MediaRating? trakt { get; set; }
public MediaRating? overall { get; set; }
}

View File

@@ -0,0 +1,25 @@
using System;
namespace StreamCinemaLib.API;
public class MediaSource
{
public string? original_language { get; set; } // two letter ISO code
public List<string>? languages { get; set; }
public MediaRatings? ratings { get; set; }
// "videos": [],
public InfoLabels? info_labels { get; set; }
public List<Cast>? cast { get; set; }
public List<InfoLabelI18n>? i18n_info_labels { get; set; }
public ServicesIds? services { get; set; }
public MediaStreamInfo? available_streams { get; set; }
// stream_info": { },
public double popularity { get; set; }
public double trending { get; set; }
public int play_count { get; set; }
public int children_count { get; set; }
public int total_children_count { get; set; }
public bool? is_concert { get; set; }
//"tags": [],
public MediaFormatInfo? streams_format_info { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System;
namespace StreamCinemaLib.API;
public class MediaStreamInfo
{
public MediaLanguageInfo? languages { get; set; }
public int count { get; set; }
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StreamCinemaLib.API;
public class Metadata
{
private const string UserAgent = "User-Agent: XBMC/19 (Linux 6.1; http://www.xbmc.org)";
private const string AccessToken = "9ajdu4xyn1ig8nxsodr3";
private const int MaxPageLimit = 1000;
private static readonly Uri ApiRoot = new Uri("https://plugin.sc2.zone/api/");
private static readonly Uri ApiFilter = new Uri(ApiRoot, "media/filter/v2/");
public static Task<FilterResponse?> SearchAsync(string expression, ItemOrder order = ItemOrder.Descending, FilterSortBy sort = FilterSortBy.Score, ItemType type = ItemType.All, int limit = 0, CancellationToken cancel = default)
{
if (expression == null)
throw new ArgumentNullException();
if (limit < 0)
throw new ArgumentOutOfRangeException();
if (limit == 0 || limit > MaxPageLimit)
limit = MaxPageLimit;
UriBuilder uri = new UriBuilder(new Uri(ApiFilter, "search"));
uri.Query = $"?access_token={AccessToken}&value={Uri.EscapeDataString(expression)}&order={ToString(order)}&sort={ToString(sort)}&type={ToString(type)}&size={limit.ToString()}";
JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateTimeConverter());
return Program._http.GetFromJsonAsync<FilterResponse>(uri.Uri, options, cancel);
}
private static string ToString(ItemOrder value)
{
switch (value)
{
case ItemOrder.Ascending: return "asc";
case ItemOrder.Descending: return "desc";
default: throw new ArgumentOutOfRangeException();
}
}
private static string ToString(FilterSortBy value)
{
switch (value)
{
case FilterSortBy.Score: return "score";
case FilterSortBy.Year: return "year";
case FilterSortBy.Premiered: return "premiered";
case FilterSortBy.DateAdded: return "dateAdded";
case FilterSortBy.LastChildrenDateAdded: return "lastChildrenDateAdded";
case FilterSortBy.LastChildPremiered: return "lastChildPremiered";
case FilterSortBy.Title: return "title";
case FilterSortBy.PlayCount: return "playCount";
case FilterSortBy.LastSeen: return "lastSeen";
case FilterSortBy.Episode: return "episode";
case FilterSortBy.News: return "news";
case FilterSortBy.Popularity: return "popularity";
case FilterSortBy.Trending: return "trending";
case FilterSortBy.LangDateAdded: return "langDateAdded";
case FilterSortBy.Custom: return "custom";
default: throw new ArgumentOutOfRangeException();
}
}
private static string ToString(ItemType value)
{
switch (value)
{
case ItemType.TVShow: return "tvshow";
case ItemType.Concert: return "concert";
case ItemType.Anime: return "anime";
case ItemType.Movie: return "movie";
case ItemType.Season: return "season";
case ItemType.Episode: return "episode";
case ItemType.All: return "*";
default: throw new ArgumentOutOfRangeException();
}
}
sealed class CustomDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace StreamCinemaLib.API;
public class ServicesIds
{
public string? csfd { get; set; }
public string? imdb { get; set; }
public string? trakt { get; set; }
public string? trakt_with_type { get; set; }
public string? tvdb { get; set; }
public string? slug { get; set; }
public string? tmdb { get; set; }
}

View File

@@ -0,0 +1,11 @@
using System;
namespace StreamCinemaLib.API;
public class ShardsInfo
{
public int total { get; set; }
public int successful { get; set; }
public int skipped { get; set; }
public int failed { get; set; }
}

View File

@@ -1,5 +1,5 @@
using System;
using JellyfinCinemaStream.Webshare;
using StreamCinema.Webshare;
class Program
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Text;
namespace JellyfinCinemaStream.Webshare;
namespace StreamCinema.Webshare;
/// <summary>
/// Performs generic binary-to-text encoding.

View File

@@ -3,7 +3,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Xml;
namespace JellyfinCinemaStream.Webshare;
namespace StreamCinema.Webshare;
public class LinkGenerator
{

View File

@@ -0,0 +1,51 @@
using System;
using System.Buffers;
using System.Text;
using System.Web;
namespace StreamCinemaWeb.Layouts;
public abstract class BasicLayout
{
protected abstract string Title { get; }
protected abstract Task RenderContentAsync(HttpRequest req, TextWriter w);
public async Task<IResult> ExecuteAsync(HttpRequest req)
{
int statusCode = 200;
StringBuilder content = new StringBuilder();
TextWriter w = new StringWriter(content);
try
{
RenderHeader(Title, w);
await RenderContentAsync(req, w);
RenderFooter(w);
}
catch (Exception e)
{
content.Clear();
RenderHeader(Title + " - Error " + e.GetType().Name, w);
w.WriteLine("<h1>Error " + e.GetType().Name + "</h1><div><pre>" + HttpUtility.HtmlEncode(e.Message) + "</pre></div>");
RenderFooter(w);
statusCode = 500;
}
return Results.Content(content.ToString(), "text/html; charset=utf-8", null, statusCode);
}
private static void RenderHeader(string title, TextWriter w) {
w.WriteLine("<!doctype html>");
w.WriteLine("<html><head>");
w.WriteLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
w.WriteLine("<title>" + HttpUtility.HtmlEncode(title) + "</title>");
w.WriteLine("<style>");
// style go here
w.WriteLine("</style>");
w.WriteLine("</head><body>");
}
private static void RenderFooter(TextWriter w) {
w.WriteLine("</body></html>");
}
}

View File

@@ -0,0 +1,56 @@
using System;
using StreamCinemaWeb.Layouts;
using StreamCinemaLib.API;
using System.Web;
namespace StreamCinemaWeb.Pages;
public class SearchPage : BasicLayout
{
protected override string Title => "Hledat filmy a seriály";
protected override async Task RenderContentAsync(HttpRequest req, TextWriter w)
{
string expr;
if (req.Method != "POST" || (expr = req.Form["expr"]) == null || expr.Trim().Length == 0)
{
// Show search
w.WriteLine("<h1>Hledat filmy a seriály</h1>");
w.WriteLine("<form method=\"POST\" action=\"/\">");
w.WriteLine("<input type=\"text\" name=\"expr\" style=\"width:100%;margin:1em\">");
w.WriteLine("<input type=\"submit\" value=\"Hledat\">");
w.WriteLine("</form>");
}
else
{
// Execute search
FilterResponse? res = await Metadata.SearchAsync(expr, cancel: req.HttpContext.RequestAborted);
w.WriteLine("<h1>Hledání:" + HttpUtility.HtmlEncode(expr) + "</h1>");
if (res == null || res.hits == null || res.hits.hits == null || res.hits.hits.Count == 0)
{
w.WriteLine("<em>Nic nenalezeno</em>");
}
else
{
foreach (MediaInfo i in res.hits.hits)
{
if (i._source == null)
continue;
InfoLabelI18n? label = i._source.i18n_info_labels?.Where(x => x.lang == "cs").FirstOrDefault();
if (label == null)
label = i._source.i18n_info_labels?.FirstOrDefault();
if (label == null)
continue;
w.WriteLine("<div class=\"media-info\">");
w.WriteLine("<div class=\"media-art\"><img src=\"" + label?.art?.poster ?? label?.art?.fanart + "\"></div>");
w.WriteLine("<div class=\"media-title\">" + HttpUtility.HtmlEncode(label?.title) + "</div");
w.WriteLine("<div class=\"media-plot\">" + HttpUtility.HtmlEncode(label?.plot) + "</div");
w.WriteLine("</div>"); // media-info
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
using StreamCinemaWeb.Pages;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", new SearchPage().ExecuteAsync);
app.MapPost("/", new SearchPage().ExecuteAsync);
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5218",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7181;http://localhost:5218",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\StreamCinemaLib\StreamCinemaLib.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

4976
test.json Normal file

File diff suppressed because it is too large Load Diff