Adding a search API.

This commit is contained in:
Zoe Roux 2019-10-23 23:23:12 +02:00
parent 729a58ad2f
commit c84eac21d2
23 changed files with 330 additions and 31 deletions

View File

@ -1,17 +1,17 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { BrowseComponent } from './browse/browse.component'; import { BrowseComponent } from './browse/browse.component';
import { ShowDetailsComponent } from './show-details/show-details.component';
import { NotFoundComponent } from './not-found/not-found.component';
import { ShowResolverService } from './services/show-resolver.service';
import { LibraryResolverService } from './services/library-resolver.service';
import { PlayerComponent } from "./player/player.component";
import { StreamResolverService } from "./services/stream-resolver.service";
import { CollectionComponent } from "./collection/collection.component"; import { CollectionComponent } from "./collection/collection.component";
import { NotFoundComponent } from './not-found/not-found.component';
import { PlayerComponent } from "./player/player.component";
import { SearchComponent } from "./search/search.component";
import { CollectionResolverService } from "./services/collection-resolver.service"; import { CollectionResolverService } from "./services/collection-resolver.service";
import { LibraryResolverService } from './services/library-resolver.service';
import { PeopleResolverService } from "./services/people-resolver.service"; import { PeopleResolverService } from "./services/people-resolver.service";
import { SearchResolverService } from "./services/search-resolver.service";
import { ShowResolverService } from './services/show-resolver.service';
import { StreamResolverService } from "./services/stream-resolver.service";
import { ShowDetailsComponent } from './show-details/show-details.component';
const routes: Routes = [ const routes: Routes = [
{ path: "browse", component: BrowseComponent, pathMatch: "full", resolve: { shows: LibraryResolverService } }, { path: "browse", component: BrowseComponent, pathMatch: "full", resolve: { shows: LibraryResolverService } },
@ -20,6 +20,7 @@ const routes: Routes = [
{ path: "collection/:collection-slug", component: CollectionComponent, resolve: { collection: CollectionResolverService } }, { path: "collection/:collection-slug", component: CollectionComponent, resolve: { collection: CollectionResolverService } },
{ path: "people/:people-slug", component: CollectionComponent, resolve: { collection: PeopleResolverService } }, { path: "people/:people-slug", component: CollectionComponent, resolve: { collection: PeopleResolverService } },
{ path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService } }, { path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService } },
{ path: "search/:query", component: SearchComponent, resolve: { items: SearchResolverService } },
{ path: "**", component: NotFoundComponent } { path: "**", component: NotFoundComponent }
]; ];

View File

@ -15,8 +15,9 @@
</ul> </ul>
<ul class="navbar-nav flex-row ml-auto"> <ul class="navbar-nav flex-row ml-auto">
<li class="nav-item icon"> <li class="nav-item icon searchbar">
<mat-icon matTooltipPosition="below" matTooltip="Search">search</mat-icon> <input placeholder="Search" id="search" type="search" (oninit)="onUpdateValue(this.value)"/>
<mat-icon matTooltipPosition="below" matTooltip="Search" (click)="openSearch()">search</mat-icon>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="icon" routerLink="/login" routerLinkActive="active" matTooltipPosition="below" matTooltip="Login"> <a class="icon" routerLink="/login" routerLinkActive="active" matTooltipPosition="below" matTooltip="Login">

View File

@ -1,3 +1,7 @@
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins/breakpoints";
.navbar .navbar
{ {
justify-content: left; justify-content: left;
@ -35,6 +39,33 @@
color: var(--accentColor); color: var(--accentColor);
} }
.searchbar
{
border-radius: 30px;
> input
{
background: none !important;
color: white;
outline: none;
border: none;
border-bottom: 1px solid #cfcfcf;
width: 0;
padding: 0;
transition: width 0.4s ease-in-out;
&:focus, .searching
{
width: 12rem;
@include media-breakpoint-up(sm)
{
width: 20rem;
}
}
}
}
.icon .icon
{ {
padding: 8px; padding: 8px;

View File

@ -39,6 +39,19 @@ export class AppComponent
} }
}); });
} }
openSearch()
{
let input = <HTMLInputElement>document.getElementById("search");
input.value = "";
input.focus();
}
onUpdateValue(value: string)
{
console.log("Value: " + value);
}
} }
interface Library interface Library

View File

@ -19,6 +19,7 @@ import { EpisodesListComponent } from './episodes-list/episodes-list.component';
import { NotFoundComponent } from './not-found/not-found.component'; import { NotFoundComponent } from './not-found/not-found.component';
import { PlayerComponent } from './player/player.component'; import { PlayerComponent } from './player/player.component';
import { ShowDetailsComponent } from './show-details/show-details.component'; import { ShowDetailsComponent } from './show-details/show-details.component';
import { SearchComponent } from './search/search.component';
@NgModule({ @NgModule({
@ -29,7 +30,8 @@ import { ShowDetailsComponent } from './show-details/show-details.component';
ShowDetailsComponent, ShowDetailsComponent,
EpisodesListComponent, EpisodesListComponent,
PlayerComponent, PlayerComponent,
CollectionComponent CollectionComponent,
SearchComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -1,6 +1,6 @@
@import "~bootstrap//scss/functions"; @import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables"; @import "~bootstrap/scss/variables";
@import "~bootstrap/scss//mixins/breakpoints"; @import "~bootstrap/scss/mixins/breakpoints";
button button
{ {

View File

@ -0,0 +1 @@
<p>search works!</p>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchComponent } from './search.component';
describe('SearchComponent', () => {
let component: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SearchComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import { SearchResut } from "../../models/search-result";
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit
{
items: SearchResut;
constructor(private route: ActivatedRoute) { }
ngOnInit()
{
this.items = this.route.snapshot.data.items;
}
}

View File

@ -0,0 +1,26 @@
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { EMPTY, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { SearchResut } from "../../models/search-result";
@Injectable({
providedIn: 'root'
})
export class SearchResolverService implements Resolve<SearchResut>
{
constructor(private http: HttpClient, private snackBar: MatSnackBar) { }
resolve(route: ActivatedRouteSnapshot): SearchResut | Observable<SearchResut> | Promise<SearchResut>
{
let query: string = route.paramMap.get("query");
return this.http.get<SearchResut>("api/search/" + query).pipe(catchError((error: HttpErrorResponse) =>
{
console.log(error.status + " - " + error.message);
this.snackBar.open("An unknow error occured.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 });
return EMPTY;
}));
}
}

View File

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=search-result.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"search-result.js","sourceRoot":"","sources":["search-result.ts"],"names":[],"mappings":""}

View File

@ -0,0 +1,14 @@
import { Show } from "./show";
import { Episode } from "./episode";
import { People } from "./people";
import { Studio } from "./studio";
import { Genre } from "./genre";
export interface SearchResut
{
shows: Show[];
episodes: Episode[];
people: People[];
genres: Genre[];
studios: Studio[];
}

View File

@ -0,0 +1,32 @@
using Kyoo.InternalAPI;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class SearchController : ControllerBase
{
private readonly ILibraryManager libraryManager;
public SearchController(ILibraryManager libraryManager)
{
this.libraryManager = libraryManager;
}
[HttpGet("{query}")]
public ActionResult<SearchResult> Search(string query)
{
SearchResult result = new SearchResult
{
shows = libraryManager.GetShows(query),
episodes = libraryManager.SearchEpisodes(query),
people = libraryManager.SearchPeople(query),
genres = libraryManager.SearchGenres(query),
studios = libraryManager.SearchStudios(query)
};
return result;
}
}
}

View File

@ -134,7 +134,6 @@ namespace Kyoo.Controllers
lines[1] = lines[1].Replace(',', '.'); lines[1] = lines[1].Replace(',', '.');
if (lines[2].Length > 5) if (lines[2].Length > 5)
{ {
Debug.WriteLine("&Line2 sub: " + lines[2].Substring(0, 6));
switch (lines[2].Substring(0, 6)) switch (lines[2].Substring(0, 6))
{ {
case "{\\an1}": case "{\\an1}":

View File

@ -280,7 +280,7 @@ namespace Kyoo.InternalAPI
case ".ass": case ".ass":
codec = "ass"; codec = "ass";
break; break;
case ".str": case ".srt":
codec = "subrip"; codec = "subrip";
break; break;
default: default:

View File

@ -8,7 +8,6 @@ namespace Kyoo.InternalAPI
{ {
//Read values //Read values
string GetShowExternalIDs(long showID); string GetShowExternalIDs(long showID);
IEnumerable<Show> GetShows();
Studio GetStudio(long showID); Studio GetStudio(long showID);
List<People> GetDirectors(long showID); List<People> GetDirectors(long showID);
List<People> GetPeople(long showID); List<People> GetPeople(long showID);
@ -25,6 +24,8 @@ namespace Kyoo.InternalAPI
Track GetSubtitle(string showSlug, long seasonNumber, long episodeNumber, string languageTag, bool forced); Track GetSubtitle(string showSlug, long seasonNumber, long episodeNumber, string languageTag, bool forced);
//Public read //Public read
IEnumerable<Show> GetShows();
IEnumerable<Show> GetShows(string searchQuery);
Library GetLibrary(string librarySlug); Library GetLibrary(string librarySlug);
IEnumerable<Library> GetLibraries(); IEnumerable<Library> GetLibraries();
Show GetShowBySlug(string slug); Show GetShowBySlug(string slug);
@ -38,6 +39,10 @@ namespace Kyoo.InternalAPI
Studio GetStudioBySlug(string slug); Studio GetStudioBySlug(string slug);
Collection GetCollection(string slug); Collection GetCollection(string slug);
IEnumerable<Episode> GetAllEpisodes(); IEnumerable<Episode> GetAllEpisodes();
IEnumerable<Episode> SearchEpisodes(string searchQuery);
IEnumerable<People> SearchPeople(string searchQuery);
IEnumerable<Genre> SearchGenres(string searchQuery);
IEnumerable<Studio> SearchStudios(string searchQuery);
//Check if value exists //Check if value exists
bool IsCollectionRegistered(string collectionSlug); bool IsCollectionRegistered(string collectionSlug);

View File

@ -302,6 +302,22 @@ namespace Kyoo.InternalAPI
return shows; return shows;
} }
public IEnumerable<Show> GetShows(string searchQuery)
{
List<Show> shows = new List<Show>();
SQLiteDataReader reader;
string query = "SELECT slug, title, aliases, startYear, endYear, '0' FROM (SELECT slug, title, aliases, startYear, endYear, '0' FROM shows LEFT JOIN collectionsLinks l ON l.showID = shows.id WHERE l.showID IS NULL UNION SELECT slug, name, null, startYear, endYear, '1' FROM collections) WHERE title LIKE $query OR aliases LIKE $query ORDER BY title;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$query", "%" + searchQuery + "%");
reader = cmd.ExecuteReader();
while (reader.Read())
shows.Add(Show.FromQueryReader(reader, true));
}
return shows;
}
public Show GetShowBySlug(string slug) public Show GetShowBySlug(string slug)
{ {
string query = "SELECT * FROM shows WHERE slug = $slug;"; string query = "SELECT * FROM shows WHERE slug = $slug;";
@ -661,6 +677,70 @@ namespace Kyoo.InternalAPI
return episodes; return episodes;
} }
} }
public IEnumerable<Episode> SearchEpisodes(string searchQuery)
{
List<Episode> episodes = new List<Episode>();
SQLiteDataReader reader;
string query = "SELECT * FROM episodes WHERE title LIKE $query ORDER BY seasonNumber, episodeNumber;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$query", "%" + searchQuery + "%");
reader = cmd.ExecuteReader();
while (reader.Read())
episodes.Add(Episode.FromReader(reader));
}
return episodes;
}
public IEnumerable<People> SearchPeople(string searchQuery)
{
List<People> people = new List<People>();
SQLiteDataReader reader;
string query = "SELECT * FROM people WHERE name LIKE $query ORDER BY name;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$query", "%" + searchQuery + "%");
reader = cmd.ExecuteReader();
while (reader.Read())
people.Add(People.FromReader(reader));
}
return people;
}
public IEnumerable<Genre> SearchGenres(string searchQuery)
{
List<Genre> genres = new List<Genre>();
SQLiteDataReader reader;
string query = "SELECT * FROM genres WHERE name LIKE $query ORDER BY name;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$query", "%" + searchQuery + "%");
reader = cmd.ExecuteReader();
while (reader.Read())
genres.Add(Genre.FromReader(reader));
}
return genres;
}
public IEnumerable<Studio> SearchStudios(string searchQuery)
{
List<Studio> studios = new List<Studio>();
SQLiteDataReader reader;
string query = "SELECT * FROM studios WHERE name LIKE $query ORDER BY name;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$query", "%" + searchQuery + "%");
reader = cmd.ExecuteReader();
while (reader.Read())
studios.Add(Studio.FromReader(reader));
}
return studios;
}
#endregion #endregion
#region Check if items exists #region Check if items exists

View File

@ -25,37 +25,49 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
string localBackdrop = Path.Combine(show.Path, "backdrop.jpg"); string localBackdrop = Path.Combine(show.Path, "backdrop.jpg");
if (show.ImgPrimary != null) if (show.ImgPrimary != null && !File.Exists(localThumb))
{ {
if (!File.Exists(localThumb)) try
{ {
using (WebClient client = new WebClient()) using (WebClient client = new WebClient())
{ {
await client.DownloadFileTaskAsync(new Uri(show.ImgPrimary), localThumb); await client.DownloadFileTaskAsync(new Uri(show.ImgPrimary), localThumb);
} }
} }
catch (WebException)
{
Console.Error.WriteLine("Couldn't download an image.");
}
} }
if (show.ImgLogo != null) if (show.ImgLogo != null && !File.Exists(localLogo))
{ {
if (!File.Exists(localLogo)) try
{ {
using (WebClient client = new WebClient()) using (WebClient client = new WebClient())
{ {
await client.DownloadFileTaskAsync(new Uri(show.ImgLogo), localLogo); await client.DownloadFileTaskAsync(new Uri(show.ImgLogo), localLogo);
} }
} }
catch (WebException)
{
Console.Error.WriteLine("Couldn't download an image.");
}
} }
if (show.ImgBackdrop != null) if (show.ImgBackdrop != null && !File.Exists(localBackdrop))
{ {
if (!File.Exists(localBackdrop)) try
{ {
using (WebClient client = new WebClient()) using (WebClient client = new WebClient())
{ {
await client.DownloadFileTaskAsync(new Uri(show.ImgBackdrop), localBackdrop); await client.DownloadFileTaskAsync(new Uri(show.ImgBackdrop), localBackdrop);
} }
} }
catch (WebException)
{
Console.Error.WriteLine("Couldn't download an image.");
}
} }
return show; return show;
@ -70,13 +82,19 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
string localThumb = root + "/" + people[i].slug + ".jpg"; string localThumb = root + "/" + people[i].slug + ".jpg";
if (people[i].imgPrimary != null && !File.Exists(localThumb)) if (people[i].imgPrimary != null && !File.Exists(localThumb))
{
try
{ {
using (WebClient client = new WebClient()) using (WebClient client = new WebClient())
{ {
Debug.WriteLine("&" + localThumb);
await client.DownloadFileTaskAsync(new Uri(people[i].imgPrimary), localThumb); await client.DownloadFileTaskAsync(new Uri(people[i].imgPrimary), localThumb);
} }
} }
catch (WebException)
{
Console.Error.WriteLine("Couldn't download an image.");
}
}
} }
return people; return people;
@ -87,12 +105,19 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
//string localThumb = Path.ChangeExtension(episode.Path, "jpg"); //string localThumb = Path.ChangeExtension(episode.Path, "jpg");
string localThumb = episode.Path.Replace(Path.GetExtension(episode.Path), "-thumb.jpg"); string localThumb = episode.Path.Replace(Path.GetExtension(episode.Path), "-thumb.jpg");
if (episode.ImgPrimary != null && !File.Exists(localThumb)) if (episode.ImgPrimary != null && !File.Exists(localThumb))
{
try
{ {
using (WebClient client = new WebClient()) using (WebClient client = new WebClient())
{ {
await client.DownloadFileTaskAsync(new Uri(episode.ImgPrimary), localThumb); await client.DownloadFileTaskAsync(new Uri(episode.ImgPrimary), localThumb);
} }
} }
catch (WebException)
{
Console.Error.WriteLine("Couldn't download an image.");
}
}
return episode; return episode;
} }

View File

@ -37,6 +37,7 @@
<None Remove="ClientApp\src\models\collection.ts" /> <None Remove="ClientApp\src\models\collection.ts" />
<None Remove="ClientApp\src\models\genre.ts" /> <None Remove="ClientApp\src\models\genre.ts" />
<None Remove="ClientApp\src\models\people.ts" /> <None Remove="ClientApp\src\models\people.ts" />
<None Remove="ClientApp\src\models\search-result.ts" />
<None Remove="ClientApp\src\models\show.ts" /> <None Remove="ClientApp\src\models\show.ts" />
<None Remove="ClientApp\src\models\studio.ts" /> <None Remove="ClientApp\src\models\studio.ts" />
<None Remove="ClientApp\src\models\watch-item.ts" /> <None Remove="ClientApp\src\models\watch-item.ts" />
@ -77,6 +78,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<TypeScriptCompile Include="ClientApp\src\models\search-result.ts">
<SubType>Code</SubType>
</TypeScriptCompile>
<TypeScriptCompile Include="ClientApp\src\models\collection.ts" /> <TypeScriptCompile Include="ClientApp\src\models\collection.ts" />
<TypeScriptCompile Include="ClientApp\src\models\genre.ts" /> <TypeScriptCompile Include="ClientApp\src\models\genre.ts" />
<TypeScriptCompile Include="ClientApp\src\models\people.ts" /> <TypeScriptCompile Include="ClientApp\src\models\people.ts" />

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Kyoo.Models
{
public class SearchResult
{
public IEnumerable<Show> shows;
public IEnumerable<Episode> episodes;
public IEnumerable<People> people;
public IEnumerable<Genre> genres;
public IEnumerable<Studio> studios;
}
}

View File

@ -91,9 +91,9 @@ namespace Kyoo.Models
IsCollection = false; IsCollection = false;
} }
public static Show FromQueryReader(System.Data.SQLite.SQLiteDataReader reader) public static Show FromQueryReader(System.Data.SQLite.SQLiteDataReader reader, bool containsAliases = false)
{ {
return new Show() Show show = new Show()
{ {
Slug = reader["slug"] as string, Slug = reader["slug"] as string,
Title = reader["title"] as string, Title = reader["title"] as string,
@ -101,6 +101,9 @@ namespace Kyoo.Models
EndYear = reader["endYear"] as long?, EndYear = reader["endYear"] as long?,
IsCollection = reader["'0'"] as string == "1" IsCollection = reader["'0'"] as string == "1"
}; };
if (containsAliases)
show.Aliases = (reader["aliases"] as string)?.Split('|');
return show;
} }
public static Show FromReader(System.Data.SQLite.SQLiteDataReader reader) public static Show FromReader(System.Data.SQLite.SQLiteDataReader reader)