diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f9488e6a..29fca9c6 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,20 +1,14 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {LibraryItemGridComponent} from './components/library-item-grid/library-item-grid.component'; -import {CollectionComponent} from "./collection/collection.component"; import {NotFoundComponent} from './not-found/not-found.component'; -import {PlayerComponent} from "./pages/player/player.component"; -import {SearchComponent} from "./pages/search/search.component"; -import {CollectionResolverService} from "./services/resolvers/collection-resolver.service"; import {PageResolver} from './services/resolvers/page-resolver.service'; -import {PeopleResolverService} from "./services/resolvers/people-resolver.service"; -import {SearchResolverService} from "./services/resolvers/search-resolver.service"; -import {ShowResolverService} from './services/resolvers/show-resolver.service'; -import {StreamResolverService} from "./services/resolvers/stream-resolver.service"; import {ShowDetailsComponent} from './pages/show-details/show-details.component'; import {AuthGuard} from "./auth/misc/authenticated-guard.service"; import {LibraryItem} from "../models/library-item"; import {LibraryItemService, LibraryService} from "./services/api.service"; +import {Show} from "../models/show"; +import {ItemResolver} from "./services/resolvers/item-resolver.service"; const routes: Routes = [ {path: "browse", component: LibraryItemGridComponent, pathMatch: "full", @@ -28,11 +22,16 @@ const routes: Routes = [ canActivate: [AuthGuard.forPermissions("read")] }, - {path: "show/:show-slug", component: ShowDetailsComponent, resolve: { show: ShowResolverService }, canLoad: [AuthGuard.forPermissions("read")], canActivate: [AuthGuard.forPermissions("read")]}, - {path: "collection/:collection-slug", component: CollectionComponent, resolve: { collection: CollectionResolverService }, canLoad: [AuthGuard.forPermissions("read")], canActivate: [AuthGuard.forPermissions("read")]}, - {path: "people/:people-slug", component: CollectionComponent, resolve: { collection: PeopleResolverService }, canLoad: [AuthGuard.forPermissions("read")], canActivate: [AuthGuard.forPermissions("read")]}, - {path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService }, canLoad: [AuthGuard.forPermissions("play")], canActivate: [AuthGuard.forPermissions("play")]}, - {path: "search/:query", component: SearchComponent, resolve: { items: SearchResolverService }, canLoad: [AuthGuard.forPermissions("read")], canActivate: [AuthGuard.forPermissions("read")]}, + {path: "show/:slug", component: ShowDetailsComponent, + resolve: { show: ItemResolver.forResource("shows/:slug") }, + canLoad: [AuthGuard.forPermissions("read")], + canActivate: [AuthGuard.forPermissions("read")] + }, + // {path: "collection/:collection-slug", component: CollectionComponent, resolve: { collection: CollectionResolverService }, canLoad: [AuthGuard.forPermissions("read")], canActivate: [AuthGuard.forPermissions("read")]}, + // + // {path: "people/:people-slug", component: CollectionComponent, resolve: { collection: PeopleResolverService }, canLoad: [AuthGuard.forPermissions("read")], canActivate: [AuthGuard.forPermissions("read")]}, + // {path: "watch/:item", component: PlayerComponent, resolve: { item: StreamResolverService }, canLoad: [AuthGuard.forPermissions("play")], canActivate: [AuthGuard.forPermissions("play")]}, + // {path: "search/:query", component: SearchComponent, resolve: { items: SearchResolverService }, canLoad: [AuthGuard.forPermissions("read")], canActivate: [AuthGuard.forPermissions("read")]}, {path: "**", component: NotFoundComponent} ]; @@ -46,11 +45,7 @@ const routes: Routes = [ LibraryService, LibraryItemService, PageResolver.resolvers, - ShowResolverService, - CollectionResolverService, - PeopleResolverService, - StreamResolverService, - SearchResolverService + ItemResolver.resolvers, ] }) export class AppRoutingModule { } diff --git a/src/app/components/episodes-list/episodes-list.component.html b/src/app/components/episodes-list/episodes-list.component.html index 052bdf86..27e6ac21 100644 --- a/src/app/components/episodes-list/episodes-list.component.html +++ b/src/app/components/episodes-list/episodes-list.component.html @@ -1,6 +1,6 @@
-
+
diff --git a/src/app/components/episodes-list/episodes-list.component.ts b/src/app/components/episodes-list/episodes-list.component.ts index b1eab54c..6cc1de6e 100644 --- a/src/app/components/episodes-list/episodes-list.component.ts +++ b/src/app/components/episodes-list/episodes-list.component.ts @@ -1,56 +1,22 @@ -import { Component, ElementRef, Input, ViewChild } from '@angular/core'; -import { MatButton } from "@angular/material/button"; +import { Component, Input} from '@angular/core'; import { DomSanitizer } from "@angular/platform-browser"; import { Episode } from "../../../models/episode"; +import {HorizontalScroller} from "../../misc/horizontal-scroller"; +import {Page} from "../../../models/page"; @Component({ selector: 'app-episodes-list', templateUrl: './episodes-list.component.html', styleUrls: ['./episodes-list.component.scss'] }) -export class EpisodesListComponent +export class EpisodesListComponent extends HorizontalScroller { @Input() displayShowTitle: boolean = false; - @Input() episodes: Episode[]; - @ViewChild("scrollView", { static: true }) private scrollView: ElementRef; - @ViewChild("leftBtn", { static: false }) private leftBtn: MatButton; - @ViewChild("rightBtn", { static: false }) private rightBtn: MatButton; - @ViewChild("episodeDom", { static: false }) private episode: ElementRef; + @Input() episodes: Page; - constructor(private sanitizer: DomSanitizer) { } - - scrollLeft() + constructor(private sanitizer: DomSanitizer) { - let scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80); - this.scrollView.nativeElement.scrollBy({ top: 0, left: -scroll, behavior: "smooth" }); - } - - scrollRight() - { - let scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80); - this.scrollView.nativeElement.scrollBy({ top: 0, left: scroll, behavior: "smooth" }); - } - - roundScroll(offset: number): number - { - let episodeSize: number = this.episode.nativeElement.scrollWidth; - - offset = Math.round(offset / episodeSize) * episodeSize; - if (offset == 0) - offset = episodeSize; - return offset; - } - - onScroll() - { - if (this.scrollView.nativeElement.scrollLeft <= 0) - this.leftBtn._elementRef.nativeElement.classList.add("d-none"); - else - this.leftBtn._elementRef.nativeElement.classList.remove("d-none"); - if (this.scrollView.nativeElement.scrollLeft >= this.scrollView.nativeElement.scrollWidth - this.scrollView.nativeElement.clientWidth) - this.rightBtn._elementRef.nativeElement.classList.add("d-none"); - else - this.rightBtn._elementRef.nativeElement.classList.remove("d-none"); + super(); } sanitize(url: string) diff --git a/src/app/components/library-item-grid/library-item-grid.component.ts b/src/app/components/library-item-grid/library-item-grid.component.ts index a3214403..0a13a0c9 100644 --- a/src/app/components/library-item-grid/library-item-grid.component.ts +++ b/src/app/components/library-item-grid/library-item-grid.component.ts @@ -16,7 +16,7 @@ export class LibraryItemGridComponent @Input() page: Page; @Input() sortEnabled: boolean = true; sortType: string = "title"; - sortKeys: string[] = ["title", "start year", "end year", "status", "type"] + sortKeys: string[] = ["title", "start year", "end year"] sortUp: boolean = true; constructor(private route: ActivatedRoute, @@ -48,7 +48,7 @@ export class LibraryItemGridComponent this.sortType = type; this.sortUp = order; - this.items.getAll({sort: `${this.sortType.replace(/\s/g, "")}:${this.sortUp ? "asc" : "desc"}`}) + this.items.getAll({sortBy: `${this.sortType.replace(/\s/g, "")}:${this.sortUp ? "asc" : "desc"}`}) .subscribe(x => this.page = x); } } diff --git a/src/app/misc/horizontal-scroller.ts b/src/app/misc/horizontal-scroller.ts new file mode 100644 index 00000000..f9e675c3 --- /dev/null +++ b/src/app/misc/horizontal-scroller.ts @@ -0,0 +1,44 @@ +import {ElementRef, ViewChild} from "@angular/core"; +import {MatButton} from "@angular/material/button"; + +export class HorizontalScroller +{ + @ViewChild("scrollView", { static: true }) private scrollView: ElementRef; + @ViewChild("leftBtn", { static: false }) private leftBtn: MatButton; + @ViewChild("rightBtn", { static: false }) private rightBtn: MatButton; + @ViewChild("itemsDom", { static: false }) private items: ElementRef; + + scrollLeft() + { + let scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80); + this.scrollView.nativeElement.scrollBy({ top: 0, left: -scroll, behavior: "smooth" }); + } + + scrollRight() + { + let scroll: number = this.roundScroll(this.scrollView.nativeElement.offsetWidth * 0.80); + this.scrollView.nativeElement.scrollBy({ top: 0, left: scroll, behavior: "smooth" }); + } + + roundScroll(offset: number): number + { + let itemSize: number = this.items.nativeElement.scrollWidth; + + offset = Math.round(offset / itemSize) * itemSize; + if (offset == 0) + offset = itemSize; + return offset; + } + + onScroll() + { + if (this.scrollView.nativeElement.scrollLeft <= 0) + this.leftBtn._elementRef.nativeElement.classList.add("d-none"); + else + this.leftBtn._elementRef.nativeElement.classList.remove("d-none"); + if (this.scrollView.nativeElement.scrollLeft >= this.scrollView.nativeElement.scrollWidth - this.scrollView.nativeElement.clientWidth) + this.rightBtn._elementRef.nativeElement.classList.add("d-none"); + else + this.rightBtn._elementRef.nativeElement.classList.remove("d-none"); + } +} \ No newline at end of file diff --git a/src/app/pages/show-details/show-details.component.html b/src/app/pages/show-details/show-details.component.html index 11a7cc61..9cd8ddf7 100644 --- a/src/app/pages/show-details/show-details.component.html +++ b/src/app/pages/show-details/show-details.component.html @@ -78,12 +78,12 @@
Season - - {{season.title}} + + {{season.title}}
- +
diff --git a/src/app/pages/show-details/show-details.component.ts b/src/app/pages/show-details/show-details.component.ts index 8634f15a..4d1cbaf1 100644 --- a/src/app/pages/show-details/show-details.component.ts +++ b/src/app/pages/show-details/show-details.component.ts @@ -1,4 +1,3 @@ -import { HttpClient } from "@angular/common/http"; import { Component, OnInit } from '@angular/core'; import { MatSnackBar } from "@angular/material/snack-bar"; import { Title } from '@angular/platform-browser'; @@ -8,7 +7,9 @@ import { Show } from "../../../models/show"; import {MatDialog} from "@angular/material/dialog"; import {TrailerDialogComponent} from "../trailer-dialog/trailer-dialog.component"; import {MetadataEditComponent} from "../metadata-edit/metadata-edit.component"; -import {Account} from "../../../models/account"; +import {Season} from "../../../models/season"; +import {EpisodeService, SeasonService} from "../../services/api.service"; +import {Page} from "../../../models/page"; @Component({ selector: 'app-show-details', @@ -18,17 +19,24 @@ import {Account} from "../../../models/account"; export class ShowDetailsComponent implements OnInit { show: Show; - episodes: Episode[] = null; - season: number; + seasons: Season[]; + season: number = 1; + episodes: Page[] = []; private toolbar: HTMLElement; private backdrop: HTMLElement; - constructor(private route: ActivatedRoute, private http: HttpClient, private snackBar: MatSnackBar, private title: Title, private router: Router, private dialog: MatDialog) + constructor(private route: ActivatedRoute, + private snackBar: MatSnackBar, + private title: Title, + private router: Router, + private dialog: MatDialog, + private seasonService: SeasonService, + private episodeService: EpisodeService) { this.route.queryParams.subscribe(params => { - this.season = params["season"]; + this.season = params["season"] ?? 1; }); this.route.data.subscribe(data => @@ -36,10 +44,19 @@ export class ShowDetailsComponent implements OnInit this.show = data.show; this.title.setTitle(this.show.title + " - Kyoo"); - if (this.season == undefined || this.show.seasons == undefined || this.show.seasons.find(x => x.seasonNumber == this.season) == null) - this.season = 1; + if (this.show.isMovie) + return; - this.getEpisodes(); + this.seasonService.getForShow(this.show.slug, {limit: 0}).subscribe(x => + { + this.seasons = x.items; + if (x.items.find(x => x.seasonNumber == this.season) == null) + { + this.season = 1; + this.getEpisodes(1); + } + }); + this.getEpisodes(this.season); }); } @@ -72,23 +89,17 @@ export class ShowDetailsComponent implements OnInit this.router.navigate(["/watch/" + this.show.slug + "-s1e1"]); } - getEpisodes() + getEpisodes(season: number) { - if (this.show == undefined || this.show.seasons == undefined) + if (season < 0) return; - if (this.show.seasons.find(x => x.seasonNumber == this.season).episodes != null) - this.episodes = this.show.seasons.find(x => x.seasonNumber == this.season).episodes; + if (this.episodes[season] != undefined) + return; - - this.http.get("api/episodes/" + this.show.slug + "/season/" + this.season).subscribe((episodes: Episode[]) => + this.episodeService.getFromSeasonNumber(this.show.slug, this.season).subscribe(x => { - this.show.seasons.find(x => x.seasonNumber == this.season).episodes = episodes; - this.episodes = episodes; - }, error => - { - console.log(error.status + " - " + error.message); - this.snackBar.open("An unknow error occured while getting episodes.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); + this.episodes[season] = x; }); } @@ -108,10 +119,10 @@ export class ShowDetailsComponent implements OnInit redownloadImages() { - this.http.post("api/show/download-images/" + this.show.slug, undefined).subscribe(() => { }, error => - { - console.log(error.status + " - " + error.message); - this.snackBar.open("An unknown error occured while re-downloading images.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); - }); + // this.http.post("api/show/download-images/" + this.show.slug, undefined).subscribe(() => { }, error => + // { + // console.log(error.status + " - " + error.message); + // this.snackBar.open("An unknown error occured while re-downloading images.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); + // }); } } diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 2937ed4e..5d698eb2 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -6,24 +6,38 @@ import {IResource} from "../../models/resources/resource"; import {Library} from "../../models/library"; import {LibraryItem} from "../../models/library-item"; import {map} from "rxjs/operators"; +import {Season} from "../../models/season"; +import {Episode} from "../../models/episode"; + +export interface ApiArgs +{ + sortBy?: string; + limit?: number; + afterID?: number; + [key: string]: any; +} class CrudApi { - constructor(private client: HttpClient, private route: string) {} + constructor(protected client: HttpClient, private route: string) {} get(id: number | string): Observable { return this.client.get(`/api/${this.route}/${id}`); } - getAll(args: {sort: string} = null): Observable> + protected ArgsAsQuery(args: ApiArgs): string { - let params: string = "?"; - if (args && args.sort) - params += "sortBy=" + args.sort; - if (params == "?") - params = ""; - return this.client.get>(`/api/${this.route}${params}`) + if (args == null) + return ""; + let params: string = Object.keys(args).map(x => `${x}=${args[x]}`).join("&"); + + return params ? `?${params}` : ""; + } + + getAll(args?: ApiArgs): Observable> + { + return this.client.get>(`/api/${this.route}${this.ArgsAsQuery(args)}`) .pipe(map(x => Object.assign(new Page(), x))); } @@ -64,3 +78,43 @@ export class LibraryItemService extends CrudApi super(client, "items"); } } + +@Injectable({ + providedIn: 'root' +}) +export class SeasonService extends CrudApi +{ + constructor(client: HttpClient) + { + super(client, "seasons"); + } + + getForShow(show: string | number, args?: ApiArgs): Observable> + { + return this.client.get(`/api/show/${show}/seasons${this.ArgsAsQuery(args)}`) + .pipe(map(x => Object.assign(new Page(), x))); + } +} + +@Injectable({ + providedIn: 'root' +}) +export class EpisodeService extends CrudApi +{ + constructor(client: HttpClient) + { + super(client, "episodes"); + } + + getFromSeason(season: string | number, args?: ApiArgs): Observable> + { + return this.client.get(`/api/seasons/${season}/episodes${this.ArgsAsQuery(args)}`) + .pipe(map(x => Object.assign(new Page(), x))); + } + + getFromSeasonNumber(show: string | number, seasonNumber: number, args?: ApiArgs): Observable> + { + return this.client.get(`/api/seasons/${show}-${seasonNumber}/episodes${this.ArgsAsQuery(args)}`) + .pipe(map(x => Object.assign(new Page(), x))); + } +} diff --git a/src/app/services/resolvers/item-resolver.service.ts b/src/app/services/resolvers/item-resolver.service.ts new file mode 100644 index 00000000..e815ef00 --- /dev/null +++ b/src/app/services/resolvers/item-resolver.service.ts @@ -0,0 +1,44 @@ +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 {Observable, EMPTY} from 'rxjs'; +import {catchError} from 'rxjs/operators'; +import {IResource} from "../../../models/resources/resource"; + +@Injectable() +export class ItemResolver +{ + public static resolvers: any[] = []; + + static forResource(resource: string) + { + @Injectable() + class Resolver implements Resolve + { + constructor(private http: HttpClient, + private snackBar: MatSnackBar) + { } + + resolve(route: ActivatedRouteSnapshot): T | Observable | Promise + { + let res: string = resource.replace(/:(.*?)(\/|$)/, (x, y) => `${route.paramMap.get(y)}/`); + + return this.http.get(`api/${res}`) + .pipe( + catchError((error: HttpErrorResponse) => + { + console.log(error.status + " - " + error.message); + this.snackBar.open(`An unknown error occurred: ${error.message}.`, null, { + horizontalPosition: "left", + panelClass: ['snackError'], + duration: 2500 + }); + return EMPTY; + })); + } + } + ItemResolver.resolvers.push(Resolver); + return Resolver; + } +} diff --git a/src/app/services/resolvers/page-resolver.service.ts b/src/app/services/resolvers/page-resolver.service.ts index bfbacef0..ecd671f7 100644 --- a/src/app/services/resolvers/page-resolver.service.ts +++ b/src/app/services/resolvers/page-resolver.service.ts @@ -23,7 +23,7 @@ export class PageResolver resolve(route: ActivatedRouteSnapshot): Page | Observable> | Promise> { - let res: string = resource.replace(/:(.*?)\//, (x, y) => `${route.paramMap.get(y)}/`); + let res: string = resource.replace(/:(.*?)(\/|$)/, (x, y) => `${route.paramMap.get(y)}/`); return this.http.get>(`api/${res}`) .pipe( diff --git a/src/app/services/resolvers/show-resolver.service.ts b/src/app/services/resolvers/show-resolver.service.ts index 7a1bd6db..c240cbb5 100644 --- a/src/app/services/resolvers/show-resolver.service.ts +++ b/src/app/services/resolvers/show-resolver.service.ts @@ -13,15 +13,15 @@ export class ShowResolverService implements Resolve resolve(route: ActivatedRouteSnapshot): Show | Observable | Promise { - let slug: string = route.paramMap.get("show-slug"); - return this.http.get("api/shows/" + slug).pipe(catchError((error: HttpErrorResponse) => - { - console.log(error.status + " - " + error.message); - if (error.status == 404) - this.snackBar.open("Show \"" + slug + "\" not found.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); - else - this.snackBar.open("An unknown error occured.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); - return EMPTY; - })); + let slug: string = route.paramMap.get("show-slug"); + return this.http.get("api/shows/" + slug).pipe(catchError((error: HttpErrorResponse) => + { + console.log(error.status + " - " + error.message); + if (error.status == 404) + this.snackBar.open("Show \"" + slug + "\" not found.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); + else + this.snackBar.open("An unknown error occured.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 }); + return EMPTY; + })); } } diff --git a/src/models/episode.ts b/src/models/episode.ts index 8253a14e..0463e428 100644 --- a/src/models/episode.ts +++ b/src/models/episode.ts @@ -1,6 +1,7 @@ import {ExternalID} from "./external-id"; +import {IResource} from "./resources/resource"; -export interface Episode +export interface Episode extends IResource { seasonNumber: number; episodeNumber: number; diff --git a/src/models/season.ts b/src/models/season.ts index c84ee74f..3f0eaa74 100644 --- a/src/models/season.ts +++ b/src/models/season.ts @@ -1,7 +1,8 @@ import { Episode } from "./episode"; import {ExternalID} from "./external-id"; +import {IResource} from "./resources/resource"; -export interface Season +export interface Season extends IResource { seasonNumber: number; title: string; diff --git a/src/models/show.ts b/src/models/show.ts index 093c5854..df9686dd 100644 --- a/src/models/show.ts +++ b/src/models/show.ts @@ -1,12 +1,12 @@ -import { Season } from "./season"; -import { Genre } from "./genre"; -import { People } from "./people"; -import { Studio } from "./studio"; +import {Season} from "./season"; +import {Genre} from "./genre"; +import {People} from "./people"; +import {Studio} from "./studio"; import {ExternalID} from "./external-id"; +import {IResource} from "./resources/resource"; -export interface Show +export interface Show extends IResource { - slug: string; title: string; aliases: string[]; overview: string; @@ -16,7 +16,6 @@ export interface Show people: People[]; seasons: Season[]; trailerUrl: string; - isCollection: boolean; isMovie: boolean; startYear: number; endYear : number;