Implementing seasons inside the web app.

Implementing episodes inside the API.
This commit is contained in:
Zoe Roux 2019-09-02 00:41:21 +02:00
parent 3a378a0ac9
commit d5358ad685
20 changed files with 183 additions and 15 deletions

View File

@ -11,7 +11,9 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -28,7 +30,8 @@ import { MatIconModule } from '@angular/material/icon';
MatSnackBarModule, MatSnackBarModule,
MatProgressBarModule, MatProgressBarModule,
MatButtonModule, MatButtonModule,
MatIconModule MatIconModule,
MatSelectModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { Show } from "../../models/show";
@Component({ @Component({
selector: 'app-browse', selector: 'app-browse',

View File

@ -3,6 +3,7 @@ import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/r
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, EMPTY } from 'rxjs'; import { Observable, EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { Show } from "../../models/show";
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';

View File

@ -3,6 +3,7 @@ import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/r
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, EMPTY } from 'rxjs'; import { Observable, EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { Show } from "../../models/show";
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';

View File

@ -13,6 +13,9 @@
<button mat-mini-fab data-toggle="tooltip" data-placement="top" title="Play" class="mr-3"> <button mat-mini-fab data-toggle="tooltip" data-placement="top" title="Play" class="mr-3">
<mat-icon>play_arrow</mat-icon> <mat-icon>play_arrow</mat-icon>
</button> </button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Trailer">
<mat-icon>local_movies</mat-icon>
</button>
<button mat-icon-button data-toggle="tooltip" data-placement="top" title="Download"> <button mat-icon-button data-toggle="tooltip" data-placement="top" title="Download">
<mat-icon>cloud_download</mat-icon> <mat-icon>cloud_download</mat-icon>
</button> </button>
@ -58,6 +61,19 @@
</div> </div>
</div> </div>
<div class="container-fluid">
<mat-form-field>
<mat-label>Season</mat-label>
<mat-select [(value)]="season">
<mat-option *ngFor="let season of this.show.seasons" [value]="season.seasonNumber">{{season.title}}</mat-option>
</mat-select>
</mat-form-field>
<p>You selected: {{season}}</p>
</div>
<!--<div class="container-fluid"> <!--<div class="container-fluid">
<div class="row"> <div class="row">
<h3 class="col">Staff</h3> <h3 class="col">Staff</h3>

View File

@ -29,12 +29,12 @@ a
@include media-breakpoint-up(lg) @include media-breakpoint-up(lg)
{ {
margin-top: -20rem; margin-top: -19rem;
} }
@include media-breakpoint-up(xl) @include media-breakpoint-up(xl)
{ {
margin-top: -24rem; margin-top: -23rem;
} }
} }

View File

@ -1,6 +1,11 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { Show } from "../../models/show";
import { Episode } from "../../models/episode";
import { HttpErrorResponse, HttpClient } from "@angular/common/http";
import { catchError } from "rxjs/operators";
import { MatSnackBar } from "@angular/material/snack-bar";
@Component({ @Component({
selector: 'app-show-details', selector: 'app-show-details',
@ -10,20 +15,32 @@ import { DomSanitizer } from '@angular/platform-browser';
export class ShowDetailsComponent implements OnInit export class ShowDetailsComponent implements OnInit
{ {
show: Show; show: Show;
season;
private toolbar: HTMLElement private toolbar: HTMLElement
private backdrop: HTMLElement private backdrop: HTMLElement
constructor(private route: ActivatedRoute, private sanitizer: DomSanitizer) { } constructor(private route: ActivatedRoute, private sanitizer: DomSanitizer, private http: HttpClient, private snackBar: MatSnackBar)
{
this.route.queryParams.subscribe(params =>
{
this.season = params["season"];
});
}
ngOnInit() ngOnInit()
{ {
this.show = this.route.snapshot.data.show; this.show = this.route.snapshot.data.show;
if (this.season == null || this.show.seasons.find(x => x.seasonNumber == this.season) == null)
this.season = this.show.seasons[0].seasonNumber;
this.toolbar = document.getElementById("toolbar"); this.toolbar = document.getElementById("toolbar");
this.backdrop = document.getElementById("backdrop"); this.backdrop = document.getElementById("backdrop");
window.addEventListener("scroll", this.scroll, true); window.addEventListener("scroll", this.scroll, true);
this.toolbar.setAttribute("style", `background-color: rgba(0, 0, 0, 0) !important`); this.toolbar.setAttribute("style", `background-color: rgba(0, 0, 0, 0) !important`);
this.getEpisodes();
} }
ngOnDestroy() ngOnDestroy()
@ -37,6 +54,22 @@ export class ShowDetailsComponent implements OnInit
this.toolbar.setAttribute("style", `background-color: rgba(0, 0, 0, ${opacity}) !important`); this.toolbar.setAttribute("style", `background-color: rgba(0, 0, 0, ${opacity}) !important`);
} }
getEpisodes()
{
console.log("getting episodes");
this.http.get<Episode[]>("api/episodes/" + this.show.slug + "/season/" + this.season).subscribe((episodes: Episode[]) =>
{
console.log(episodes.length);
}, error =>
{
console.log(error.status + " - " + error.message);
this.snackBar.open("An unknow error occured while getting episodes.", null, { horizontalPosition: "left", panelClass: ['snackError'], duration: 2500 });
});
console.log("Episodes got");
}
getPeopleIcon(slug: string) getPeopleIcon(slug: string)

View File

@ -0,0 +1,9 @@
export interface Episode
{
episodeNumber: number;
title: string;
overview: string;
releaseDate;
runtimeInMinutes: number;
externalIDs: string;
}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import { Episode } from "./episode";
export interface Season
{
seasonNumber: number;
title: string;
overview: string;
episode: Episode[];
}

View File

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

View File

@ -1,4 +1,6 @@
interface Show import { Season } from "./season";
export interface Show
{ {
id: number; id: number;
slug: string; slug: string;
@ -18,4 +20,6 @@ interface Show
imgBackdrop: string; imgBackdrop: string;
externalIDs: string; externalIDs: string;
seasons: Season[];
} }

View File

@ -32,7 +32,7 @@ $font-family-base: "Roboto", Arial, sans-serif;
// Define the default theme (same as the example above). // Define the default theme (same as the example above).
$primary: (default: #0a1128); $primary: (default: #0a1128);
$accent: (default: #e23c00, lighter: #ff9149); $accent: (default: #e23c00, lighter: #ff9149);
$theme: mat-light-theme($primary, $accent); $theme: mat-dark-theme($primary, $accent);
// Include the default theme styles. // Include the default theme styles.
@include angular-material-theme($theme); @include angular-material-theme($theme);

View File

@ -2,6 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.InternalAPI;
using Kyoo.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -11,9 +13,33 @@ namespace Kyoo.Controllers
[ApiController] [ApiController]
public class EpisodesController : ControllerBase public class EpisodesController : ControllerBase
{ {
public string Index() private readonly ILibraryManager libraryManager;
public EpisodesController(ILibraryManager libraryManager)
{ {
return "Episode Test"; this.libraryManager = libraryManager;
}
[HttpGet("{showSlug}/season/{seasonNumber}")]
public ActionResult<IEnumerable<Episode>> GetEpisodesForSeason(string showSlug, long seasonNumber)
{
List<Episode> episodes = libraryManager.GetEpisodes(showSlug, seasonNumber);
if(episodes == null)
return NotFound();
return episodes;
}
[HttpGet("{showSlug}/season/{seasonNumber}/episode/{episodeNumber}")]
public ActionResult<Episode> GetEpisode(string showSlug, long seasonNumber, long episodeNumber)
{
Episode episode = libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null)
return NotFound();
return episode;
} }
} }
} }

View File

@ -2,7 +2,6 @@
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {

View File

@ -18,6 +18,8 @@ namespace Kyoo.InternalAPI
IEnumerable<Library> GetLibraries(); IEnumerable<Library> GetLibraries();
Show GetShowBySlug(string slug); Show GetShowBySlug(string slug);
Season GetSeason(string showSlug, long seasonNumber); Season GetSeason(string showSlug, long seasonNumber);
List<Episode> GetEpisodes(string showSlug, long seasonNumber);
Episode GetEpisode(string showSlug, long seasonNumber, long episodeNumber);
People GetPeopleBySlug(string slug); People GetPeopleBySlug(string slug);
Genre GetGenreBySlug(string slug); Genre GetGenreBySlug(string slug);
Studio GetStudioBySlug(string slug); Studio GetStudioBySlug(string slug);

View File

@ -56,6 +56,7 @@ namespace Kyoo.InternalAPI
id INTEGER PRIMARY KEY UNIQUE, id INTEGER PRIMARY KEY UNIQUE,
showID INTEGER, showID INTEGER,
seasonID INTEGER, seasonID INTEGER,
seasonNumber INTEGER,
episodeNumber INTEGER, episodeNumber INTEGER,
path TEXT, path TEXT,
title TEXT, title TEXT,
@ -264,6 +265,43 @@ namespace Kyoo.InternalAPI
} }
} }
public List<Episode> GetEpisodes(string showSlug, long seasonNumber)
{
string query = "SELECT * FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$showSlug", showSlug);
cmd.Parameters.AddWithValue("$seasonNumber", seasonNumber);
SQLiteDataReader reader = cmd.ExecuteReader();
List<Episode> episodes = new List<Episode>();
while (reader.Read())
episodes.Add(Episode.FromReader(reader));
return episodes;
}
}
public Episode GetEpisode(string showSlug, long seasonNumber, long episodeNumber)
{
string query = "SELECT * FROM episodes JOIN shows ON shows.id = episodes.showID WHERE shows.slug = $showSlug AND episodes.seasonNumber = $seasonNumber AND episodes.episodeNumber = $episodeNumber;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$showSlug", showSlug);
cmd.Parameters.AddWithValue("$seasonNumber", seasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episodeNumber);
SQLiteDataReader reader = cmd.ExecuteReader();
if (reader.Read())
return Episode.FromReader(reader);
else
return null;
}
}
public List<People> GetPeople(long showID) public List<People> GetPeople(long showID)
{ {
string query = "SELECT people.id, people.slug, people.name, people.imgPrimary, people.externalIDs, l.role, l.type FROM people JOIN peopleLinks l ON l.peopleID = people.id WHERE l.showID = $showID;"; string query = "SELECT people.id, people.slug, people.name, people.imgPrimary, people.externalIDs, l.role, l.type FROM people JOIN peopleLinks l ON l.peopleID = people.id WHERE l.showID = $showID;";
@ -550,11 +588,12 @@ namespace Kyoo.InternalAPI
public long RegisterEpisode(Episode episode) public long RegisterEpisode(Episode episode)
{ {
string query = "INSERT INTO episodes (showID, seasonID, episodeNumber, path, title, overview, releaseDate, runtime, imgPrimary, externalIDs) VALUES($showID, $seasonID, $episodeNumber, $path, $title, $overview, $releaseDate, $runtime, $imgPrimary, $externalIDs);"; string query = "INSERT INTO episodes (showID, seasonID, seasonNumber, episodeNumber, path, title, overview, releaseDate, runtime, imgPrimary, externalIDs) VALUES($showID, $seasonID, $seasonNumber,$episodeNumber, $path, $title, $overview, $releaseDate, $runtime, $imgPrimary, $externalIDs);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{ {
cmd.Parameters.AddWithValue("$showID", episode.ShowID); cmd.Parameters.AddWithValue("$showID", episode.ShowID);
cmd.Parameters.AddWithValue("$seasonID", episode.SeasonID); cmd.Parameters.AddWithValue("$seasonID", episode.SeasonID);
cmd.Parameters.AddWithValue("$seasonNUmber", episode.seasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber); cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber);
cmd.Parameters.AddWithValue("$path", episode.Path); cmd.Parameters.AddWithValue("$path", episode.Path);
cmd.Parameters.AddWithValue("$title", episode.Title); cmd.Parameters.AddWithValue("$title", episode.Title);

View File

@ -307,7 +307,7 @@ namespace Kyoo.InternalAPI.MetadataProvider
dynamic episode = data.data[index]; dynamic episode = data.data[index];
DateTime dateTime = DateTime.ParseExact((string)episode.firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture); DateTime dateTime = DateTime.ParseExact((string)episode.firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture);
return new Episode(episodeNumber, (string)episode.episodeName, (string)episode.overview, dateTime, -1, "https://www.thetvdb.com/banners/" + episode.filename, string.Format("TvDB={0}|", episode.id)); return new Episode(seasonNumber, episodeNumber, (string)episode.episodeName, (string)episode.overview, dateTime, -1, "https://www.thetvdb.com/banners/" + episode.filename, string.Format("TvDB={0}|", episode.id));
} }
} }
else else

View File

@ -9,11 +9,12 @@ namespace Kyoo.Models
[JsonIgnore] public long ShowID; [JsonIgnore] public long ShowID;
[JsonIgnore] public long SeasonID; [JsonIgnore] public long SeasonID;
public long seasonNumber;
public long episodeNumber; public long episodeNumber;
[JsonIgnore] public string Path; [JsonIgnore] public string Path;
public string Title; public string Title;
public string Overview; public string Overview;
public DateTime ReleaseDate; public DateTime? ReleaseDate;
public long Runtime; //This runtime variable should be in seconds (used by the video manager so we need precisions) public long Runtime; //This runtime variable should be in seconds (used by the video manager so we need precisions)
@ -31,11 +32,12 @@ namespace Kyoo.Models
public Episode() { } public Episode() { }
public Episode(long episodeNumber, string title, string overview, DateTime releaseDate, long runtime, string imgPrimary, string externalIDs) public Episode(long seasonNumber, long episodeNumber, string title, string overview, DateTime? releaseDate, long runtime, string imgPrimary, string externalIDs)
{ {
id = -1; id = -1;
ShowID = -1; ShowID = -1;
SeasonID = -1; SeasonID = -1;
this.seasonNumber = seasonNumber;
this.episodeNumber = episodeNumber; this.episodeNumber = episodeNumber;
Title = title; Title = title;
Overview = overview; Overview = overview;
@ -45,11 +47,12 @@ namespace Kyoo.Models
ExternalIDs = externalIDs; ExternalIDs = externalIDs;
} }
public Episode(long id, long showID, long seasonID, long episodeNumber, string path, string title, string overview, DateTime releaseDate, long runtime, string imgPrimary, string externalIDs) public Episode(long id, long showID, long seasonID, long seasonNumber, long episodeNumber, string path, string title, string overview, DateTime? releaseDate, long runtime, string imgPrimary, string externalIDs)
{ {
this.id = id; this.id = id;
ShowID = showID; ShowID = showID;
SeasonID = seasonID; SeasonID = seasonID;
this.seasonNumber = seasonNumber;
this.episodeNumber = episodeNumber; this.episodeNumber = episodeNumber;
Path = path; Path = path;
Title = title; Title = title;
@ -59,5 +62,21 @@ namespace Kyoo.Models
ImgPrimary = imgPrimary; ImgPrimary = imgPrimary;
ExternalIDs = externalIDs; ExternalIDs = externalIDs;
} }
public static Episode FromReader(System.Data.SQLite.SQLiteDataReader reader)
{
return new Episode((long)reader["id"],
(long)reader["showID"],
(long)reader["seasonID"],
(long)reader["seasonNumber"],
(long)reader["episodeNumber"],
reader["path"] as string,
reader["title"] as string,
reader["overview"] as string,
reader["releaseDate"] as DateTime?,
(long)reader["runtime"],
reader["imgPrimary"] as string,
reader["externalIDs"] as string);
}
} }
} }