mirror of
https://github.com/devfake/flox.git
synced 2024-11-23 18:42:30 +01:00
subpage design (#64)
* start with subpage design * transitions * transitions * transitions, stuff * modal for trailer * english fallback for trailers * slugs in url * bit refactor * tests and fixtures * travis * update production files * move time limit to config and display error message to user * fix import * fix review
This commit is contained in:
parent
fd2ed4676b
commit
619e407bcd
@ -4,6 +4,9 @@ TRANSLATION=
|
||||
CLIENT_URI=/
|
||||
LOADING_ITEMS=30
|
||||
|
||||
# Default 10 minutes (600 seconds)
|
||||
PHP_TIME_LIMIT=600
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
|
@ -25,34 +25,6 @@
|
||||
'release_season',
|
||||
];
|
||||
|
||||
/**
|
||||
* Save all episodes of each season.
|
||||
*
|
||||
* @param $seasons
|
||||
* @param $tmdbId
|
||||
*/
|
||||
public function store($seasons, $tmdbId)
|
||||
{
|
||||
foreach($seasons as $season) {
|
||||
$releaseSeason = Carbon::createFromFormat('Y-m-d', $season->air_date ?? '1970-12-1');
|
||||
|
||||
foreach($season->episodes as $episode) {
|
||||
$releaseEpisode = Carbon::createFromFormat('Y-m-d', $episode->air_date ?? '1970-12-1');
|
||||
|
||||
$this->create([
|
||||
'season_tmdb_id' => $season->id,
|
||||
'episode_tmdb_id' => $episode->id,
|
||||
'season_number' => $episode->season_number,
|
||||
'episode_number' => $episode->episode_number,
|
||||
'release_episode' => $releaseEpisode->getTimestamp(),
|
||||
'release_season' => $releaseSeason->getTimestamp(),
|
||||
'name' => $episode->name,
|
||||
'tmdb_id' => $tmdbId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessors
|
||||
*/
|
||||
|
@ -5,6 +5,7 @@ namespace App\Exceptions;
|
||||
use Exception;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Symfony\Component\Debug\Exception\FatalErrorException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@ -40,10 +41,17 @@ class Handler extends ExceptionHandler
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $exception
|
||||
* @return \Illuminate\Http\Response
|
||||
*
|
||||
* @return \Illuminate\Http\Response|string
|
||||
*/
|
||||
public function render($request, Exception $exception)
|
||||
{
|
||||
if($exception instanceof FatalErrorException) {
|
||||
echo $exception->getMessage();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
use App\AlternativeTitle;
|
||||
use App\Episode;
|
||||
use App\Item;
|
||||
use App\Services\Models\ItemService;
|
||||
use App\Services\Storage;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
@ -49,9 +50,10 @@
|
||||
* Reset item table and restore backup.
|
||||
* Downloads every poster image new.
|
||||
*
|
||||
* @param ItemService $itemService
|
||||
* @return Response
|
||||
*/
|
||||
public function import()
|
||||
public function import(ItemService $itemService)
|
||||
{
|
||||
increaseTimeLimit();
|
||||
|
||||
@ -65,6 +67,13 @@
|
||||
|
||||
$data = json_decode(file_get_contents($file));
|
||||
|
||||
$this->importItems($data, $itemService);
|
||||
$this->importEpisodes($data);
|
||||
$this->importAlternativeTitles($data);
|
||||
}
|
||||
|
||||
private function importItems($data, ItemService $itemService)
|
||||
{
|
||||
if(isset($data->items)) {
|
||||
$this->item->truncate();
|
||||
foreach($data->items as $item) {
|
||||
@ -73,18 +82,31 @@
|
||||
$item->last_seen_at = Carbon::createFromTimestamp($item->created_at);
|
||||
}
|
||||
|
||||
// For empty items (from file-parser) we don't need access to details.
|
||||
if($item->tmdb_id) {
|
||||
$item = $itemService->makeDataComplete((array) $item);
|
||||
|
||||
$this->storage->downloadPoster($item['poster']);
|
||||
$this->storage->downloadBackdrop($item['backdrop']);
|
||||
}
|
||||
|
||||
$this->item->create((array) $item);
|
||||
$this->storage->downloadPoster($item->poster);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function importEpisodes($data)
|
||||
{
|
||||
if(isset($data->episodes)) {
|
||||
$this->episodes->truncate();
|
||||
foreach($data->episodes as $episode) {
|
||||
$this->episodes->create((array) $episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function importAlternativeTitles($data)
|
||||
{
|
||||
if(isset($data->alternative_titles)) {
|
||||
$this->alternativeTitles->truncate();
|
||||
foreach($data->alternative_titles as $title) {
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\FileParser;
|
||||
use App\Setting;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@ -27,12 +26,12 @@
|
||||
public function call()
|
||||
{
|
||||
try {
|
||||
$files = $this->parser->fetch();
|
||||
$this->parser->fetch();
|
||||
} catch(ConnectException $e) {
|
||||
return response("Can't connect to file-parser. Make sure the server is running.", Response::HTTP_NOT_FOUND);
|
||||
} catch(\Exception $e) {
|
||||
return response("Error in file-parser:" . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return $this->updateDatabase($files);
|
||||
}
|
||||
|
||||
/**
|
||||
|
19
backend/app/Http/Controllers/SubpageController.php
Normal file
19
backend/app/Http/Controllers/SubpageController.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\IMDB;
|
||||
use App\Services\Subpage;
|
||||
|
||||
class SubpageController {
|
||||
|
||||
public function item($tmdbId, $mediaType, Subpage $subpage)
|
||||
{
|
||||
return $subpage->item($tmdbId, $mediaType);
|
||||
}
|
||||
|
||||
public function imdbRating($id, IMDB $imdb)
|
||||
{
|
||||
return $imdb->parseRating($id);
|
||||
}
|
||||
}
|
@ -24,6 +24,13 @@
|
||||
'last_seen_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'overview',
|
||||
'tmdb_rating',
|
||||
'imdb_id',
|
||||
'imdb_rating',
|
||||
'backdrop',
|
||||
'youtube_key',
|
||||
'slug',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -43,7 +50,14 @@
|
||||
'rating' => 0,
|
||||
'released' => $data['released'],
|
||||
'genre' => $data['genre'],
|
||||
'overview' => $data['overview'],
|
||||
'backdrop' => $data['backdrop'],
|
||||
'tmdb_rating' => $data['tmdb_rating'],
|
||||
'imdb_id' => $data['imdb_id'],
|
||||
'imdb_rating' => $data['imdb_rating'],
|
||||
'youtube_key' => $data['youtube_key'],
|
||||
'last_seen_at' => Carbon::now(),
|
||||
'slug' => $data['slug'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,6 @@
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class FileParser {
|
||||
@ -167,7 +166,7 @@
|
||||
{
|
||||
$found = $this->searchTmdb($file);
|
||||
|
||||
// Remove the empty item, because we create a new empty or from TMDb.
|
||||
// We will create a new empty or a real item from TMDb.
|
||||
$this->itemService->remove($emptyItem->id);
|
||||
|
||||
if( ! $found) {
|
||||
|
27
backend/app/Services/IMDB.php
Normal file
27
backend/app/Services/IMDB.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use DiDom\Document;
|
||||
|
||||
class IMDB {
|
||||
|
||||
private $document;
|
||||
|
||||
public function __construct(Document $document)
|
||||
{
|
||||
$this->document = $document;
|
||||
}
|
||||
|
||||
public function parseRating($id = null)
|
||||
{
|
||||
$document = $this->document->loadHtmlFile(config('services.imdb.url') . $id);
|
||||
|
||||
// We don't need to check if we found a result if we loop over them.
|
||||
foreach($document->find('.ratingValue strong span') as $rating) {
|
||||
return $rating->text();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -33,10 +33,28 @@
|
||||
*/
|
||||
public function create($item)
|
||||
{
|
||||
// todo: rewrite this and make this more generic. we need to use this in refresh too, if a episode is not in database.
|
||||
if($item->media_type == 'tv') {
|
||||
$seasons = $this->tmdb->tvEpisodes($item->tmdb_id);
|
||||
|
||||
$this->model->store($seasons, $item->tmdb_id);
|
||||
foreach($seasons as $season) {
|
||||
$releaseSeason = Carbon::createFromFormat('Y-m-d', $season->air_date ?? '1970-12-1');
|
||||
|
||||
foreach($season->episodes as $episode) {
|
||||
$releaseEpisode = Carbon::createFromFormat('Y-m-d', $episode->air_date ?? '1970-12-1');
|
||||
|
||||
$this->model->create([
|
||||
'season_tmdb_id' => $season->id,
|
||||
'episode_tmdb_id' => $episode->id,
|
||||
'season_number' => $episode->season_number,
|
||||
'episode_number' => $episode->episode_number,
|
||||
'release_episode' => $releaseEpisode->getTimestamp(),
|
||||
'release_season' => $releaseSeason->getTimestamp(),
|
||||
'name' => $episode->name,
|
||||
'tmdb_id' => $item->tmdb_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Item as Model;
|
||||
use App\Item;
|
||||
use App\Services\IMDB;
|
||||
use App\Services\Storage;
|
||||
use App\Services\TMDB;
|
||||
|
||||
@ -14,6 +15,7 @@
|
||||
private $storage;
|
||||
private $alternativeTitleService;
|
||||
private $episodeService;
|
||||
private $imdb;
|
||||
|
||||
/**
|
||||
* @param Model $model
|
||||
@ -21,19 +23,22 @@
|
||||
* @param Storage $storage
|
||||
* @param AlternativeTitleService $alternativeTitleService
|
||||
* @param EpisodeService $episodeService
|
||||
* @param IMDB $imdb
|
||||
*/
|
||||
public function __construct(
|
||||
Model $model,
|
||||
TMDB $tmdb,
|
||||
Storage $storage,
|
||||
AlternativeTitleService $alternativeTitleService,
|
||||
EpisodeService $episodeService
|
||||
EpisodeService $episodeService,
|
||||
IMDB $imdb
|
||||
){
|
||||
$this->model = $model;
|
||||
$this->tmdb = $tmdb;
|
||||
$this->storage = $storage;
|
||||
$this->alternativeTitleService = $alternativeTitleService;
|
||||
$this->episodeService = $episodeService;
|
||||
$this->imdb = $imdb;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,17 +47,99 @@
|
||||
*/
|
||||
public function create($data)
|
||||
{
|
||||
$data = $this->makeDataComplete($data);
|
||||
|
||||
$item = $this->model->store($data);
|
||||
|
||||
$this->storage->downloadPoster($item->poster);
|
||||
|
||||
$this->episodeService->create($item);
|
||||
|
||||
$this->alternativeTitleService->create($item);
|
||||
|
||||
$this->storage->downloadPoster($item->poster);
|
||||
$this->storage->downloadBackdrop($item->backdrop);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search against TMDb and IMDb for more informations.
|
||||
* We don't need to get more informations if we add the item from the subpage.
|
||||
*
|
||||
* @param $data
|
||||
* @return array
|
||||
*/
|
||||
public function makeDataComplete($data)
|
||||
{
|
||||
if( ! isset($data['imdb_id'])) {
|
||||
$details = $this->tmdb->details($data['tmdb_id'], $data['media_type']);
|
||||
$title = $details->name ?? $details->title;
|
||||
|
||||
$data['imdb_id'] = $data['imdb_id'] ?? $this->parseImdbId($details);
|
||||
$data['youtube_key'] = $data['youtube_key'] ?? $this->parseYoutubeKey($details, $data['media_type']);
|
||||
$data['overview'] = $data['overview'] ?? $details->overview;
|
||||
$data['tmdb_rating'] = $data['tmdb_rating'] ?? $details->vote_average;
|
||||
$data['backdrop'] = $data['backdrop'] ?? $details->backdrop_path;
|
||||
$data['slug'] = $data['slug'] ?? (str_slug($title) != '' ? str_slug($title) : 'no-slug-available');
|
||||
}
|
||||
|
||||
$data['imdb_rating'] = $this->parseImdbRating($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user clicks to fast on adding item,
|
||||
* we need to re-fetch the rating from IMDb.
|
||||
*
|
||||
* @param $data
|
||||
*
|
||||
* @return float|null
|
||||
*/
|
||||
private function parseImdbRating($data)
|
||||
{
|
||||
if( ! isset($data['imdb_rating'])) {
|
||||
$imdbId = $data['imdb_id'];
|
||||
|
||||
if($imdbId) {
|
||||
return $this->imdb->parseRating($imdbId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise we already have the rating saved.
|
||||
return $data['imdb_rating'];
|
||||
}
|
||||
|
||||
/**
|
||||
* TV shows needs an extra append for external ids.
|
||||
*
|
||||
* @param $details
|
||||
* @return mixed
|
||||
*/
|
||||
public function parseImdbId($details)
|
||||
{
|
||||
return $details->external_ids->imdb_id ?? ($details->imdb_id ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for the youtube trailer video. Fallback with english trailer.
|
||||
*
|
||||
* @param $details
|
||||
* @param $mediaType
|
||||
* @return string|null
|
||||
*/
|
||||
public function parseYoutubeKey($details, $mediaType)
|
||||
{
|
||||
if(isset($details->videos->results[0])) {
|
||||
return $details->videos->results[0]->key;
|
||||
}
|
||||
|
||||
// Try to fetch details again with english language as fallback.
|
||||
$videos = $this->tmdb->videos($details->id, $mediaType, 'en');
|
||||
|
||||
return $videos->results[0]->key ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @param $mediaType
|
||||
@ -60,7 +147,7 @@
|
||||
*/
|
||||
public function createEmpty($data, $mediaType)
|
||||
{
|
||||
$mediaType = $mediaType == 'movies' ? 'movie' : 'tv';
|
||||
$mediaType = mediaType($mediaType);
|
||||
|
||||
$data = [
|
||||
'name' => getFileName($data),
|
||||
@ -90,10 +177,11 @@
|
||||
|
||||
$item->delete();
|
||||
|
||||
// Delete all related episodes, alternative titles and poster image.
|
||||
// Delete all related episodes, alternative titles and images.
|
||||
$this->episodeService->remove($tmdbId);
|
||||
$this->alternativeTitleService->remove($tmdbId);
|
||||
$this->storage->removePosterFile($item->poster);
|
||||
$this->storage->removePoster($item->poster);
|
||||
$this->storage->removeBackdrop($item->backdrop);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,7 +268,7 @@
|
||||
public function findBy($type, $value, $mediaType = null)
|
||||
{
|
||||
if($mediaType) {
|
||||
$mediaType = $mediaType == 'movies' ? 'movie' : 'tv';
|
||||
$mediaType = mediaType($mediaType);
|
||||
}
|
||||
|
||||
switch($type) {
|
||||
|
@ -28,25 +28,49 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the poster image file.
|
||||
* Download the poster image files.
|
||||
*
|
||||
* @param $poster
|
||||
*/
|
||||
public function downloadPoster($poster)
|
||||
{
|
||||
if($poster) {
|
||||
LaravelStorage::put($poster, file_get_contents('http://image.tmdb.org/t/p/w185' . $poster));
|
||||
LaravelStorage::put($poster, file_get_contents(config('services.tmdb.poster') . $poster));
|
||||
LaravelStorage::disk('subpage')->put($poster, file_get_contents(config('services.tmdb.poster_subpage') . $poster));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the poster image file.
|
||||
* Download the backdrop image file.
|
||||
*
|
||||
* @param $backdrop
|
||||
*/
|
||||
public function downloadBackdrop($backdrop)
|
||||
{
|
||||
if($backdrop) {
|
||||
LaravelStorage::disk('backdrop')->put($backdrop, file_get_contents(config('services.tmdb.backdrop') . $backdrop));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the poster image files.
|
||||
*
|
||||
* @param $poster
|
||||
*/
|
||||
public function removePosterFile($poster)
|
||||
public function removePoster($poster)
|
||||
{
|
||||
LaravelStorage::delete($poster);
|
||||
LaravelStorage::disk('subpage')->delete($poster);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the backdrop image.
|
||||
*
|
||||
* @param $backdrop
|
||||
*/
|
||||
public function removeBackdrop($backdrop)
|
||||
{
|
||||
LaravelStorage::disk('backdrop')->delete($backdrop);
|
||||
}
|
||||
|
||||
/**
|
||||
|
33
backend/app/Services/Subpage.php
Normal file
33
backend/app/Services/Subpage.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\Models\ItemService;
|
||||
|
||||
class Subpage {
|
||||
|
||||
private $itemService;
|
||||
private $tmdb;
|
||||
|
||||
public function __construct(ItemService $itemService, TMDB $tmdb)
|
||||
{
|
||||
$this->itemService = $itemService;
|
||||
$this->tmdb = $tmdb;
|
||||
}
|
||||
|
||||
public function item($tmdbId, $mediaType)
|
||||
{
|
||||
if($found = $this->itemService->findBy('tmdb_id', $tmdbId)) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
$found = $this->tmdb->details($tmdbId, $mediaType);
|
||||
$found->genre_ids = collect($found->genres)->pluck('id')->all();
|
||||
|
||||
$item = $this->tmdb->createItem($found, $mediaType);
|
||||
$item['youtube_key'] = $this->itemService->parseYoutubeKey($found, $mediaType);
|
||||
$item['imdb_id'] = $this->itemService->parseImdbId($found);
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@
|
||||
$movies = collect($this->createItems($response, 'movie'));
|
||||
}
|
||||
|
||||
return $tv->merge($movies)->toArray();
|
||||
return $movies->merge($tv)->sortByDesc('popularity')->values()->all();
|
||||
}
|
||||
|
||||
private function fetchSearch($title, $mediaType) {
|
||||
@ -172,30 +172,43 @@
|
||||
$response = json_decode($response->getBody());
|
||||
|
||||
foreach($response->results as $result) {
|
||||
$release = Carbon::createFromFormat('Y-m-d', isset($result->release_date) ? ($result->release_date ?: '1970-12-1') : ($result->first_air_date ?: '1970-12-1'));
|
||||
|
||||
// 'name' is from tv, 'title' from movies
|
||||
$items[] = [
|
||||
'tmdb_id' => $result->id,
|
||||
'title' => $result->name ?? $result->title,
|
||||
'original_title' => $result->original_name ?? $result->original_title,
|
||||
'poster' => $result->poster_path,
|
||||
'media_type' => $mediaType,
|
||||
'released' => $release->getTimestamp(),
|
||||
'genre' => $this->parseGenre($result->genre_ids),
|
||||
'episodes' => [],
|
||||
];
|
||||
$items[] = $this->createItem($result, $mediaType);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function createItem($data, $mediaType)
|
||||
{
|
||||
$release = Carbon::createFromFormat('Y-m-d',
|
||||
isset($data->release_date) ? ($data->release_date ?: '1970-12-1') : ($data->first_air_date ?: '1970-12-1')
|
||||
);
|
||||
|
||||
$title = $data->name ?? $data->title;
|
||||
|
||||
return [
|
||||
'tmdb_id' => $data->id,
|
||||
'title' => $title,
|
||||
'slug' => str_slug($title) != '' ? str_slug($title) : 'no-slug-available',
|
||||
'original_title' => $data->original_name ?? $data->original_title,
|
||||
'poster' => $data->poster_path,
|
||||
'media_type' => $mediaType,
|
||||
'released' => $release->getTimestamp(),
|
||||
'genre' => $this->parseGenre($data->genre_ids),
|
||||
'episodes' => [],
|
||||
'overview' => $data->overview,
|
||||
'backdrop' => $data->backdrop_path,
|
||||
'tmdb_rating' => $data->vote_average,
|
||||
'popularity' => $data->popularity,
|
||||
];
|
||||
}
|
||||
|
||||
private function requestTmdb($url, $query = [])
|
||||
{
|
||||
$query = array_merge($query, [
|
||||
$query = array_merge([
|
||||
'api_key' => $this->apiKey,
|
||||
'language' => strtolower($this->translation)
|
||||
]);
|
||||
], $query);
|
||||
|
||||
try {
|
||||
$response = $this->client->get($url, [
|
||||
@ -219,7 +232,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full movie or tv details.
|
||||
* Get full movie or tv details with trailers.
|
||||
*
|
||||
* @param $tmdbId
|
||||
* @param $mediaType
|
||||
@ -227,7 +240,18 @@
|
||||
*/
|
||||
public function details($tmdbId, $mediaType)
|
||||
{
|
||||
$response = $this->requestTmdb($this->base . '/3/' . $mediaType . '/' . $tmdbId);
|
||||
$response = $this->requestTmdb($this->base . '/3/' . $mediaType . '/' . $tmdbId, [
|
||||
'append_to_response' => 'videos,external_ids',
|
||||
]);
|
||||
|
||||
return json_decode($response->getBody());
|
||||
}
|
||||
|
||||
public function videos($tmdbId, $mediaType, $translation = null)
|
||||
{
|
||||
$response = $this->requestTmdb($this->base . '/3/' . $mediaType . '/' . $tmdbId . '/videos', [
|
||||
'language' => $translation ?? $this->translation,
|
||||
]);
|
||||
|
||||
return json_decode($response->getBody());
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
<?php
|
||||
|
||||
const FIVE_MINUTES = 300;
|
||||
|
||||
function increaseTimeLimit()
|
||||
{
|
||||
set_time_limit(FIVE_MINUTES);
|
||||
set_time_limit(config('app.PHP_TIME_LIMIT'));
|
||||
}
|
||||
|
||||
function getFileName($file)
|
||||
{
|
||||
return $file->changed->name ?? $file->name;
|
||||
}
|
||||
|
||||
function mediaType($mediaType)
|
||||
{
|
||||
return $mediaType == 'movies' ? 'movie' : 'tv';
|
||||
}
|
@ -10,7 +10,8 @@
|
||||
"laravel/scout": "^1.1",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"algolia/algoliasearch-client-php": "^1.10",
|
||||
"doctrine/dbal": "^2.5"
|
||||
"doctrine/dbal": "^2.5",
|
||||
"imangazaliev/didom": "^1.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^0.9.7",
|
||||
@ -32,7 +33,8 @@
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/TestCase.php"
|
||||
"tests/TestCase.php",
|
||||
"tests/Traits"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
|
50
backend/composer.lock
generated
50
backend/composer.lock
generated
@ -4,8 +4,8 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "2f6f2e24c6494cfce80b24e0388f96a0",
|
||||
"content-hash": "4bb9c55b817be4946797aade8d2df60b",
|
||||
"hash": "294c93796fb9df3f91863c5c977db9af",
|
||||
"content-hash": "b6ea193eec158865d05abcdd90db9510",
|
||||
"packages": [
|
||||
{
|
||||
"name": "algolia/algoliasearch-client-php",
|
||||
@ -786,6 +786,52 @@
|
||||
],
|
||||
"time": "2016-06-24 23:00:38"
|
||||
},
|
||||
{
|
||||
"name": "imangazaliev/didom",
|
||||
"version": "1.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Imangazaliev/DiDOM.git",
|
||||
"reference": "0116a51428b6c34bcdd5d32b8674576154701196"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Imangazaliev/DiDOM/zipball/0116a51428b6c34bcdd5d32b8674576154701196",
|
||||
"reference": "0116a51428b6c34bcdd5d32b8674576154701196",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DiDom\\": "src/DiDom/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Imangazaliev Muhammad",
|
||||
"email": "imangazalievm@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Simple and fast HTML parser",
|
||||
"homepage": "https://github.com/Imangazaliev/DiDOM",
|
||||
"keywords": [
|
||||
"didom",
|
||||
"html",
|
||||
"parser",
|
||||
"xml"
|
||||
],
|
||||
"time": "2017-02-02 05:35:44"
|
||||
},
|
||||
{
|
||||
"name": "jakub-onderka/php-console-color",
|
||||
"version": "0.1",
|
||||
|
@ -8,6 +8,7 @@
|
||||
'TRANSLATION' => env('TRANSLATION', 'EN'),
|
||||
'LOADING_ITEMS' => env('LOADING_ITEMS'),
|
||||
'CLIENT_URI' => env('CLIENT_URI'),
|
||||
'PHP_TIME_LIMIT' => env('PHP_TIME_LIMIT'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -48,6 +48,16 @@
|
||||
'root' => base_path('../public/assets/poster'),
|
||||
],
|
||||
|
||||
'subpage' => [
|
||||
'driver' => 'local',
|
||||
'root' => base_path('../public/assets/poster/subpage'),
|
||||
],
|
||||
|
||||
'backdrop' => [
|
||||
'driver' => 'local',
|
||||
'root' => base_path('../public/assets/backdrop'),
|
||||
],
|
||||
|
||||
'export' => [
|
||||
'driver' => 'local',
|
||||
'root' => base_path('../public/exports'),
|
||||
|
@ -37,6 +37,13 @@ return [
|
||||
|
||||
'tmdb' => [
|
||||
'key' => env('TMDB_API_KEY'),
|
||||
'poster' => 'http://image.tmdb.org/t/p/w185',
|
||||
'poster_subpage' => 'http://image.tmdb.org/t/p/w342',
|
||||
'backdrop' => 'http://image.tmdb.org/t/p/w1280',
|
||||
],
|
||||
|
||||
'imdb' => [
|
||||
'url' => 'http://www.imdb.com/title/',
|
||||
],
|
||||
|
||||
'fp' => [
|
||||
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSubpageFieldsToItemsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('items', function (Blueprint $table) {
|
||||
$table->string('backdrop')->nullable();
|
||||
$table->string('slug')->nullable();
|
||||
$table->string('youtube_key')->nullable();
|
||||
$table->string('imdb_id')->nullable();
|
||||
$table->text('overview')->nullable();
|
||||
$table->string('tmdb_rating')->nullable();
|
||||
$table->string('imdb_rating')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('items', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
@ -8,6 +8,9 @@
|
||||
Route::get('/items/{type}/{orderBy}', 'ItemController@items');
|
||||
Route::get('/search-items', 'ItemController@search');
|
||||
|
||||
Route::get('/item/{tmdbId}/{mediaType}', 'SubpageController@item');
|
||||
Route::get('/imdb-rating/{imdbId}', 'SubpageController@imdbRating');
|
||||
|
||||
Route::get('/suggestions/{tmdbID}/{mediaType}', 'TMDBController@suggestions');
|
||||
Route::get('/trending', 'TMDBController@trending');
|
||||
Route::get('/upcoming', 'TMDBController@upcoming');
|
||||
|
@ -2,15 +2,14 @@
|
||||
|
||||
use App\AlternativeTitle;
|
||||
use App\Services\Models\AlternativeTitleService;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
|
||||
class AlternativeTitleServiceTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
use Factories;
|
||||
use Fixtures;
|
||||
use Mocks;
|
||||
|
||||
private $alternativeTitles;
|
||||
|
||||
@ -127,14 +126,4 @@
|
||||
$this->assertNotNull($titles1);
|
||||
$this->assertCount(0, $titles2);
|
||||
}
|
||||
|
||||
private function createGuzzleMock($fixture)
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture),
|
||||
]);
|
||||
|
||||
$handler = HandlerStack::create($mock);
|
||||
$this->app->instance(Client::class, new Client(['handler' => $handler]));
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,14 @@
|
||||
use App\Episode;
|
||||
use App\Item;
|
||||
use App\Services\Models\EpisodeService;
|
||||
use App\Services\Models\ItemService;
|
||||
use App\Services\TMDB;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
|
||||
class EpisodeServiceTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
use Factories;
|
||||
use Fixtures;
|
||||
use Mocks;
|
||||
|
||||
private $episode;
|
||||
private $episodeService;
|
||||
@ -29,7 +30,7 @@
|
||||
{
|
||||
$tv = $this->getTv();
|
||||
|
||||
$this->createTmdbMock();
|
||||
$this->createTmdbEpisodeMock();
|
||||
$episodeService = app(EpisodeService::class);
|
||||
|
||||
$episodes1 = $this->episode->all();
|
||||
@ -146,13 +147,4 @@
|
||||
|
||||
$this->assertEquals($itemUpdated->last_seen_at, $item->last_seen_at);
|
||||
}
|
||||
|
||||
private function createTmdbMock()
|
||||
{
|
||||
// Mock this to avoid unknown requests to TMDb (get seasons and then get episodes for each season)
|
||||
$mock = Mockery::mock(app(TMDB::class))->makePartial();
|
||||
$mock->shouldReceive('tvEpisodes')->andReturn(json_decode($this->tmdbFixtures('tv/episodes')));
|
||||
|
||||
$this->app->instance(TMDB::class, $mock);
|
||||
}
|
||||
}
|
@ -2,18 +2,16 @@
|
||||
|
||||
use App\Episode;
|
||||
use App\Item;
|
||||
use App\Services\TMDB;
|
||||
use App\Setting;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use App\Services\FileParser;
|
||||
|
||||
class FileParserTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
use Factories;
|
||||
use Fixtures;
|
||||
use Mocks;
|
||||
|
||||
private $item;
|
||||
private $parser;
|
||||
@ -26,6 +24,9 @@
|
||||
$this->item = app(Item::class);
|
||||
$this->episode = app(Episode::class);
|
||||
$this->parser = app(FileParser::class);
|
||||
|
||||
$this->createStorageDownloadsMock();
|
||||
$this->createImdbRatingMock();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@ -34,7 +35,7 @@
|
||||
$items = $this->item->get();
|
||||
$setting = Setting::first()->last_fetch_to_file_parser;
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('movie/movie'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$this->createGuzzleMock($this->tmdbFixtures('movie/movie'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('movie/unknown'));
|
||||
|
||||
@ -53,7 +54,7 @@
|
||||
$episodes = $this->episode->get();
|
||||
$setting = Setting::first()->last_fetch_to_file_parser;
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('tv/tv'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$this->createGuzzleMock($this->tmdbFixtures('tv/tv'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('tv/unknown'));
|
||||
|
||||
@ -106,7 +107,12 @@
|
||||
{
|
||||
$items = $this->item->get();
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('movie/movie'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/movie'),
|
||||
$this->tmdbFixtures('movie/details'),
|
||||
$this->tmdbFixtures('movie/alternative_titles')
|
||||
);
|
||||
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('movie/added'));
|
||||
|
||||
@ -126,7 +132,13 @@
|
||||
$items = $this->item->get();
|
||||
$episodes1 = $this->episode->get();
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('tv/tv'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/tv'),
|
||||
$this->tmdbFixtures('tv/details'),
|
||||
$this->tmdbFixtures('tv/alternative_titles')
|
||||
);
|
||||
$this->createTmdbEpisodeMock();
|
||||
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('tv/added'));
|
||||
|
||||
@ -150,7 +162,7 @@
|
||||
{
|
||||
$this->createMovie();
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('movie/movie'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$this->createGuzzleMock($this->tmdbFixtures('movie/movie'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$parser = app(FileParser::class);
|
||||
|
||||
$setting1 = Setting::first();
|
||||
@ -316,7 +328,12 @@
|
||||
|
||||
$empty = $this->item->first();
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('movie/movie'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/movie'),
|
||||
$this->tmdbFixtures('movie/details'),
|
||||
$this->tmdbFixtures('movie/alternative_titles')
|
||||
);
|
||||
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('movie/updated_found'));
|
||||
|
||||
@ -337,7 +354,13 @@
|
||||
|
||||
$empty = $this->item->first();
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('tv/tv'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/tv'),
|
||||
$this->tmdbFixtures('tv/details'),
|
||||
$this->tmdbFixtures('tv/alternative_titles')
|
||||
);
|
||||
$this->createTmdbEpisodeMock();
|
||||
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('tv/updated_found'));
|
||||
|
||||
@ -359,7 +382,7 @@
|
||||
/** @test */
|
||||
public function it_should_create_empty_movie_from_updated_if_not_found_in_tmdb()
|
||||
{
|
||||
$this->createTmdbMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$this->createGuzzleMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('movie/updated_not_found'));
|
||||
|
||||
@ -373,7 +396,7 @@
|
||||
/** @test */
|
||||
public function it_should_create_empty_tv_without_episodes_from_updated_if_not_found_in_tmdb()
|
||||
{
|
||||
$this->createTmdbMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$this->createGuzzleMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('tv/updated_not_found'));
|
||||
|
||||
@ -388,7 +411,7 @@
|
||||
/** @test */
|
||||
public function it_should_create_empty_movie_from_added_if_not_found_in_tmdb()
|
||||
{
|
||||
$this->createTmdbMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$this->createGuzzleMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('movie/alternative_titles'));
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('movie/added_not_found'));
|
||||
|
||||
@ -401,7 +424,7 @@
|
||||
/** @test */
|
||||
public function it_should_create_empty_tv_without_episodes_from_added_if_not_found_in_tmdb()
|
||||
{
|
||||
$this->createTmdbMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$this->createGuzzleMock($this->tmdbFixtures('empty'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$parser = app(FileParser::class);
|
||||
$parser->updateDatabase($this->fpFixtures('tv/added_not_found'));
|
||||
|
||||
@ -433,7 +456,13 @@
|
||||
$settings->save();
|
||||
$this->assertEquals($timestamp, Setting::first()->last_fetch_to_file_parser->timestamp);
|
||||
|
||||
$this->createTmdbMock($this->tmdbFixtures('tv/tv'), $this->tmdbFixtures('tv/alternative_titles'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/tv'),
|
||||
$this->tmdbFixtures('tv/details'),
|
||||
$this->tmdbFixtures('tv/alternative_titles')
|
||||
);
|
||||
$this->createTmdbEpisodeMock();
|
||||
|
||||
$fixture = json_encode($this->fpFixtures("tv/added"));
|
||||
|
||||
$this->call('PATCH', '/api/update-files', [], [], [], $this->http_login(), $fixture);
|
||||
@ -482,21 +511,4 @@
|
||||
'PHP_AUTH_PW' => 'snow',
|
||||
];
|
||||
}
|
||||
|
||||
private function createTmdbMock($fixture, $fixture2)
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture),
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture2),
|
||||
]);
|
||||
|
||||
$handler = HandlerStack::create($mock);
|
||||
$this->app->instance(Client::class, new Client(['handler' => $handler]));
|
||||
|
||||
// Mock this to avoid unknown requests to TMDb (get seasons and then get episodes for each season)
|
||||
$mock = Mockery::mock(app(TMDB::class))->makePartial();
|
||||
$mock->shouldReceive('tvEpisodes')->andReturn(json_decode($this->tmdbFixtures('tv/episodes')));
|
||||
|
||||
$this->app->instance(TMDB::class, $mock);
|
||||
}
|
||||
}
|
||||
|
33
backend/tests/Services/IMDBTest.php
Normal file
33
backend/tests/Services/IMDBTest.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Services\IMDB;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
|
||||
class IMDBTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
|
||||
/** @test */
|
||||
public function it_should_parse_imdb_rating()
|
||||
{
|
||||
config(['services.imdb.url' => __DIR__ . '/../fixtures/imdb/with-rating.html']);
|
||||
|
||||
$imdbService = app(IMDB::class);
|
||||
|
||||
$rating = $imdbService->parseRating();
|
||||
|
||||
$this->assertEquals('7,0', $rating);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_return_null_if_no_rating_was_found()
|
||||
{
|
||||
config(['services.imdb.url' => __DIR__ . '/../fixtures/imdb/without-rating.html']);
|
||||
|
||||
$imdbService = app(IMDB::class);
|
||||
|
||||
$rating = $imdbService->parseRating();
|
||||
|
||||
$this->assertNull($rating);
|
||||
}
|
||||
}
|
@ -1,19 +1,15 @@
|
||||
<?php
|
||||
|
||||
use App\Item;
|
||||
use App\Services\Models\AlternativeTitleService;
|
||||
use App\Services\Models\EpisodeService;
|
||||
use App\Services\Models\ItemService;
|
||||
use App\Services\Storage;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
class ItemServiceTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
use Factories;
|
||||
use Fixtures;
|
||||
use Mocks;
|
||||
|
||||
private $item;
|
||||
private $itemService;
|
||||
@ -24,14 +20,17 @@
|
||||
|
||||
$this->item = app(Item::class);
|
||||
$this->itemService = app(ItemService::class);
|
||||
|
||||
$this->createStorageDownloadsMock();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_create_a_new_movie()
|
||||
{
|
||||
$this->createEpisodeMock();
|
||||
$this->createStorageMock();
|
||||
$this->createAlternativeTitleMock();
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/details'),
|
||||
$this->tmdbFixtures('movie/alternative_titles')
|
||||
);
|
||||
|
||||
$itemService = app(ItemService::class);
|
||||
|
||||
@ -49,9 +48,12 @@
|
||||
/** @test */
|
||||
public function it_should_create_a_new_tv_show()
|
||||
{
|
||||
$this->createEpisodeMock();
|
||||
$this->createStorageMock();
|
||||
$this->createAlternativeTitleMock();
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/details'),
|
||||
$this->tmdbFixtures('tv/alternative_titles')
|
||||
);
|
||||
|
||||
$this->createTmdbEpisodeMock();
|
||||
|
||||
$itemService = app(ItemService::class);
|
||||
|
||||
@ -119,7 +121,7 @@
|
||||
/** @test */
|
||||
public function it_should_update_genre_for_a_movie()
|
||||
{
|
||||
$user = factory(App\User::class)->create();
|
||||
$user = $this->createUser();
|
||||
$this->createMovie();
|
||||
|
||||
$this->createGuzzleMock($this->tmdbFixtures('movie/details'));
|
||||
@ -135,7 +137,7 @@
|
||||
/** @test */
|
||||
public function it_should_update_genre_for_a_tv_show()
|
||||
{
|
||||
$user = factory(App\User::class)->create();
|
||||
$user = $this->createUser();
|
||||
$this->createTv();
|
||||
|
||||
$this->createGuzzleMock($this->tmdbFixtures('tv/details'));
|
||||
@ -147,41 +149,42 @@
|
||||
$this->assertEmpty($withoutGenre->genre);
|
||||
$this->assertNotEmpty($withGenre->genre);
|
||||
}
|
||||
|
||||
private function createGuzzleMock($fixture)
|
||||
|
||||
/** @test */
|
||||
public function it_should_parse_correct_imdb_id()
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture),
|
||||
]);
|
||||
$idMovie = $this->itemService->parseImdbId(json_decode($this->tmdbFixtures('movie/details')));
|
||||
$idTv = $this->itemService->parseImdbId(json_decode($this->tmdbFixtures('tv/details')));
|
||||
|
||||
$handler = HandlerStack::create($mock);
|
||||
$this->app->instance(Client::class, new Client(['handler' => $handler]));
|
||||
$this->assertEquals('tt0803096', $idMovie);
|
||||
$this->assertEquals('tt0944947', $idTv);
|
||||
}
|
||||
|
||||
private function mock($class)
|
||||
/** @test */
|
||||
public function it_should_parse_correct_youtube_key()
|
||||
{
|
||||
$mock = Mockery::mock($class)->makePartial();
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('videos'),
|
||||
$this->tmdbFixtures('videos')
|
||||
);
|
||||
|
||||
$this->app->instance($class, $mock);
|
||||
$itemService = app(ItemService::class);
|
||||
|
||||
return $mock;
|
||||
}
|
||||
$fixtureMovie = json_decode($this->tmdbFixtures('movie/details'));
|
||||
$fixtureTv = json_decode($this->tmdbFixtures('tv/details'));
|
||||
|
||||
private function createStorageMock()
|
||||
{
|
||||
$storageMock = $this->mock(Storage::class);
|
||||
$storageMock->shouldReceive('downloadPoster')->once()->andReturn(true);
|
||||
}
|
||||
$foundInDetailsMovie = $itemService->parseYoutubeKey($fixtureMovie, 'movie');
|
||||
$foundInDetailsTv = $itemService->parseYoutubeKey($fixtureTv, 'tv');
|
||||
|
||||
private function createEpisodeMock()
|
||||
{
|
||||
$episodeMock = $this->mock(EpisodeService::class);
|
||||
$episodeMock->shouldReceive('create')->once()->andReturn(true);
|
||||
}
|
||||
$fixtureMovie->videos->results = null;
|
||||
$fixtureTv->videos->results = null;
|
||||
|
||||
private function createAlternativeTitleMock()
|
||||
{
|
||||
$alternativeTitleMock = $this->mock(AlternativeTitleService::class);
|
||||
$alternativeTitleMock->shouldReceive('create')->once()->andReturn(true);
|
||||
$fallBackMovie = $itemService->parseYoutubeKey($fixtureMovie, 'movie');
|
||||
$fallBackTv = $itemService->parseYoutubeKey($fixtureMovie, 'tv');
|
||||
|
||||
$this->assertEquals('2Rxoz13Bthc', $foundInDetailsMovie);
|
||||
$this->assertEquals('BpJYNVhGf1s', $foundInDetailsTv);
|
||||
$this->assertEquals('qnIhJwhBeqY', $fallBackMovie);
|
||||
$this->assertEquals('qnIhJwhBeqY', $fallBackTv);
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,17 @@
|
||||
class TMDBTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
use Factories;
|
||||
use Fixtures;
|
||||
use Mocks;
|
||||
|
||||
/** @test */
|
||||
public function it_should_search_and_merge_movies_and_tv()
|
||||
{
|
||||
$this->createGuzzleMock($this->tmdbFixtures('tv/search'), $this->tmdbFixtures('movie/search'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/search'),
|
||||
$this->tmdbFixtures('movie/search')
|
||||
);
|
||||
|
||||
$result = $this->callSearch();
|
||||
|
||||
@ -28,7 +34,10 @@
|
||||
/** @test */
|
||||
public function it_should_only_search_for_tv()
|
||||
{
|
||||
$this->createGuzzleMock($this->tmdbFixtures('tv/search'), $this->tmdbFixtures('movie/search'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/search'),
|
||||
$this->tmdbFixtures('movie/search')
|
||||
);
|
||||
|
||||
$result = $this->callSearch('tv');
|
||||
|
||||
@ -42,7 +51,10 @@
|
||||
/** @test */
|
||||
public function it_should_only_search_for_movies()
|
||||
{
|
||||
$this->createGuzzleMock($this->tmdbFixtures('movie/search'), $this->tmdbFixtures('tv/search'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/search'),
|
||||
$this->tmdbFixtures('tv/search')
|
||||
);
|
||||
|
||||
$result = $this->callSearch('movies');
|
||||
|
||||
@ -56,7 +68,10 @@
|
||||
/** @test */
|
||||
public function it_should_fetch_and_merge_movies_and_tv_in_trending()
|
||||
{
|
||||
$this->createGuzzleMock($this->tmdbFixtures('tv/trending'), $this->tmdbFixtures('movie/trending'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/trending'),
|
||||
$this->tmdbFixtures('movie/trending')
|
||||
);
|
||||
|
||||
$tmdb = app(TMDB::class);
|
||||
$trending = $tmdb->trending();
|
||||
@ -71,7 +86,10 @@
|
||||
/** @test */
|
||||
public function it_should_merge_database_items_in_trending()
|
||||
{
|
||||
$this->createGuzzleMock($this->tmdbFixtures('tv/trending'), $this->tmdbFixtures('movie/trending'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/trending'),
|
||||
$this->tmdbFixtures('movie/trending')
|
||||
);
|
||||
|
||||
$this->createMovie();
|
||||
$this->createTv();
|
||||
@ -87,7 +105,10 @@
|
||||
/** @test */
|
||||
public function it_should_merge_database_movie_in_upcoming()
|
||||
{
|
||||
$this->createGuzzleMock($this->tmdbFixtures('movie/upcoming'), $this->tmdbFixtures('movie/upcoming'));
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/upcoming'),
|
||||
$this->tmdbFixtures('movie/upcoming')
|
||||
);
|
||||
|
||||
$this->createMovie();
|
||||
|
||||
@ -101,11 +122,9 @@
|
||||
/** @test */
|
||||
public function it_should_respect_request_limit()
|
||||
{
|
||||
$fixture = $this->tmdbFixtures('multi');
|
||||
|
||||
$mock = new MockHandler([
|
||||
new Response(429, []),
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture),
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $this->tmdbFixtures('multi')),
|
||||
]);
|
||||
|
||||
$handler = HandlerStack::create($mock);
|
||||
@ -128,15 +147,4 @@
|
||||
private function in_array_r($item , $array){
|
||||
return (bool) preg_match('/"' . $item . '"/i' , json_encode($array));
|
||||
}
|
||||
|
||||
private function createGuzzleMock($fixture, $fixture2)
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture),
|
||||
new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture2),
|
||||
]);
|
||||
|
||||
$handler = HandlerStack::create($mock);
|
||||
$this->app->instance(Client::class, new Client(['handler' => $handler]));
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
class ExportImportTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
use Factories;
|
||||
|
||||
protected $user;
|
||||
|
||||
@ -17,7 +18,7 @@
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user = factory(App\User::class)->create();
|
||||
$this->user = $this->createUser();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@ -54,9 +55,9 @@
|
||||
{
|
||||
$this->callImport('export.json');
|
||||
|
||||
$this->assertCount(5, Item::all());
|
||||
$this->assertCount(143, Episode::all());
|
||||
$this->assertCount(25, AlternativeTitle::all());
|
||||
$this->assertCount(4, Item::all());
|
||||
$this->assertCount(10, Episode::all());
|
||||
$this->assertCount(38, AlternativeTitle::all());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -6,6 +6,7 @@
|
||||
class SettingTest extends TestCase {
|
||||
|
||||
use DatabaseMigrations;
|
||||
use Factories;
|
||||
|
||||
protected $user;
|
||||
|
||||
@ -13,7 +14,7 @@
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user = factory(App\User::class)->create();
|
||||
$this->user = $this->createUser();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -12,90 +12,4 @@
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function fpFixtures($type)
|
||||
{
|
||||
return json_decode(file_get_contents(__DIR__ . '/fixtures/fp/' . $type . '.json'));
|
||||
}
|
||||
|
||||
protected function tmdbFixtures($type)
|
||||
{
|
||||
return file_get_contents(__DIR__ . '/fixtures/tmdb/' . $type . '.json');
|
||||
}
|
||||
|
||||
protected function floxFixtures($type)
|
||||
{
|
||||
return collect(json_decode(file_get_contents(__DIR__ . '/fixtures/flox/' . $type . '.json')))->toArray();
|
||||
}
|
||||
|
||||
protected function createSetting()
|
||||
{
|
||||
factory(App\Setting::class)->create();
|
||||
}
|
||||
|
||||
protected function createMovie($custom = [])
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Warcraft: The Beginning',
|
||||
'original_title' => 'Warcraft',
|
||||
'tmdb_id' => 68735,
|
||||
'media_type' => 'movie',
|
||||
];
|
||||
|
||||
factory(App\Item::class)->create(array_merge($data, $custom));
|
||||
}
|
||||
|
||||
protected function createTv($custom = [], $withEpisodes = true)
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Game of Thrones',
|
||||
'original_title' => 'Game of Thrones',
|
||||
'tmdb_id' => 1399,
|
||||
'media_type' => 'tv',
|
||||
];
|
||||
|
||||
factory(App\Item::class)->create(array_merge($data, $custom));
|
||||
|
||||
if($withEpisodes) {
|
||||
foreach([1, 2] as $season) {
|
||||
foreach([1, 2] as $episode) {
|
||||
factory(App\Episode::class)->create([
|
||||
'tmdb_id' => 1399,
|
||||
'season_number' => $season,
|
||||
'episode_number' => $episode,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getMovie($custom = [])
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Warcraft',
|
||||
'tmdb_id' => 68735,
|
||||
];
|
||||
|
||||
return factory(App\Item::class)->states('movie')->make(array_merge($data, $custom));
|
||||
}
|
||||
|
||||
protected function getTv($custom = [])
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Game of Thrones',
|
||||
'tmdb_id' => 1399,
|
||||
];
|
||||
|
||||
return factory(App\Item::class)->states('tv')->make(array_merge($data, $custom));
|
||||
}
|
||||
|
||||
protected function getMovieSrc()
|
||||
{
|
||||
return '/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv';
|
||||
}
|
||||
|
||||
protected function getTvSrc()
|
||||
{
|
||||
return '/tv/Game of Thrones/S1/1.mkv';
|
||||
}
|
||||
}
|
||||
|
70
backend/tests/Traits/Factories.php
Normal file
70
backend/tests/Traits/Factories.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
trait Factories {
|
||||
|
||||
public function createUser()
|
||||
{
|
||||
return factory(App\User::class)->create();
|
||||
}
|
||||
|
||||
public function createSetting()
|
||||
{
|
||||
return factory(App\Setting::class)->create();
|
||||
}
|
||||
|
||||
public function createMovie($custom = [])
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Warcraft: The Beginning',
|
||||
'original_title' => 'Warcraft',
|
||||
'tmdb_id' => 68735,
|
||||
'media_type' => 'movie',
|
||||
];
|
||||
|
||||
return factory(App\Item::class)->create(array_merge($data, $custom));
|
||||
}
|
||||
|
||||
public function createTv($custom = [], $withEpisodes = true)
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Game of Thrones',
|
||||
'original_title' => 'Game of Thrones',
|
||||
'tmdb_id' => 1399,
|
||||
'media_type' => 'tv',
|
||||
];
|
||||
|
||||
factory(App\Item::class)->create(array_merge($data, $custom));
|
||||
|
||||
if($withEpisodes) {
|
||||
foreach([1, 2] as $season) {
|
||||
foreach([1, 2] as $episode) {
|
||||
factory(App\Episode::class)->create([
|
||||
'tmdb_id' => 1399,
|
||||
'season_number' => $season,
|
||||
'episode_number' => $episode,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getMovie($custom = [])
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Warcraft',
|
||||
'tmdb_id' => 68735,
|
||||
];
|
||||
|
||||
return factory(App\Item::class)->states('movie')->make(array_merge($data, $custom));
|
||||
}
|
||||
|
||||
public function getTv($custom = [])
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Game of Thrones',
|
||||
'tmdb_id' => 1399,
|
||||
];
|
||||
|
||||
return factory(App\Item::class)->states('tv')->make(array_merge($data, $custom));
|
||||
}
|
||||
}
|
34
backend/tests/Traits/Fixtures.php
Normal file
34
backend/tests/Traits/Fixtures.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
trait Fixtures {
|
||||
|
||||
protected function fpFixtures($type)
|
||||
{
|
||||
return json_decode(file_get_contents(__DIR__ . '/../fixtures/fp/' . $type . '.json'));
|
||||
}
|
||||
|
||||
protected function tmdbFixtures($type)
|
||||
{
|
||||
return file_get_contents(__DIR__ . '/../fixtures/tmdb/' . $type . '.json');
|
||||
}
|
||||
|
||||
protected function imdbFixtures($type)
|
||||
{
|
||||
return file_get_contents(__DIR__ . '/../fixtures/imdb/' . $type);
|
||||
}
|
||||
|
||||
protected function floxFixtures($type)
|
||||
{
|
||||
return collect(json_decode(file_get_contents(__DIR__ . '/../fixtures/flox/' . $type . '.json')))->toArray();
|
||||
}
|
||||
|
||||
protected function getMovieSrc()
|
||||
{
|
||||
return '/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv';
|
||||
}
|
||||
|
||||
protected function getTvSrc()
|
||||
{
|
||||
return '/tv/Game of Thrones/S1/1.mkv';
|
||||
}
|
||||
}
|
55
backend/tests/Traits/Mocks.php
Normal file
55
backend/tests/Traits/Mocks.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use App\Services\IMDB;
|
||||
use App\Services\Storage;
|
||||
use App\Services\TMDB;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
trait Mocks {
|
||||
|
||||
public function createGuzzleMock()
|
||||
{
|
||||
$fixtures = func_get_args();
|
||||
$responses = [];
|
||||
|
||||
foreach($fixtures as $fixture) {
|
||||
$responses[] = new Response(200, ['X-RateLimit-Remaining' => [40]], $fixture);
|
||||
}
|
||||
|
||||
$mock = new MockHandler($responses);
|
||||
|
||||
$handler = HandlerStack::create($mock);
|
||||
$this->app->instance(Client::class, new Client(['handler' => $handler]));
|
||||
}
|
||||
|
||||
public function createStorageDownloadsMock()
|
||||
{
|
||||
$mock = $this->mock(Storage::class);
|
||||
$mock->shouldReceive('downloadPoster', 'downloadBackdrop')->andReturn(null, null);
|
||||
}
|
||||
|
||||
public function createTmdbEpisodeMock()
|
||||
{
|
||||
// Mock this to avoid unknown requests to TMDb (get seasons and then get episodes for each season)
|
||||
$mock = $this->mock(TMDB::class);
|
||||
$mock->shouldReceive('tvEpisodes')->andReturn(json_decode($this->tmdbFixtures('tv/episodes')));
|
||||
}
|
||||
|
||||
private function createImdbRatingMock()
|
||||
{
|
||||
$mock = $this->mock(IMDB::class);
|
||||
$mock->shouldReceive('parseRating')->andReturn(json_decode($this->imdbFixtures('rating.txt')));
|
||||
}
|
||||
|
||||
public function mock($class)
|
||||
{
|
||||
$mock = Mockery::mock(app($class))->makePartial();
|
||||
|
||||
$this->app->instance($class, $mock);
|
||||
|
||||
return $mock;
|
||||
}
|
||||
}
|
2
backend/tests/fixtures/flox/export.json
vendored
2
backend/tests/fixtures/flox/export.json
vendored
File diff suppressed because one or more lines are too long
36
backend/tests/fixtures/fp/tv/unknown.json
vendored
36
backend/tests/fixtures/fp/tv/unknown.json
vendored
@ -1,41 +1,5 @@
|
||||
{
|
||||
"tv": [
|
||||
{
|
||||
"name": "Game of Thrones",
|
||||
"season_number": 2,
|
||||
"episode_number": 1,
|
||||
"status": "added",
|
||||
"extension": "mkv",
|
||||
"tags": [],
|
||||
"year": null,
|
||||
"filename": "1",
|
||||
"subtitles": "src",
|
||||
"src": "/tv/Game of Thrones/S1/1.mkv"
|
||||
},
|
||||
{
|
||||
"name": "Game of Thrones",
|
||||
"season_number": 2,
|
||||
"episode_number": 2,
|
||||
"tags": [],
|
||||
"status": "added",
|
||||
"extension": "mkv",
|
||||
"year": null,
|
||||
"filename": "2",
|
||||
"subtitles": "src",
|
||||
"src": "/tv/Game of Thrones/S1/1.mkv"
|
||||
},
|
||||
{
|
||||
"name": "Game of Thrones",
|
||||
"season_number": 1,
|
||||
"episode_number": 1,
|
||||
"extension": "mkv",
|
||||
"status": "added",
|
||||
"filename": "1",
|
||||
"tags": [],
|
||||
"subtitles": "src",
|
||||
"year": null,
|
||||
"src": "/tv/Game of Thrones/s1/1.mkv"
|
||||
},
|
||||
{
|
||||
"name": "Game of Thrones",
|
||||
"season_number": 1,
|
||||
|
1
backend/tests/fixtures/imdb/rating.txt
vendored
Normal file
1
backend/tests/fixtures/imdb/rating.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
5.1
|
17
backend/tests/fixtures/imdb/with-rating.html
vendored
Normal file
17
backend/tests/fixtures/imdb/with-rating.html
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="title_block">
|
||||
<div class="title_bar_wrapper">
|
||||
|
||||
<div class="ratings_wrapper">
|
||||
<div class="imdbRating">
|
||||
<div class="ratingValue">
|
||||
<strong title="7,9 based on 466.506 user ratings">
|
||||
<span itemprop="ratingValue">7,0</span>
|
||||
</strong>
|
||||
<span class="grey">/</span>
|
||||
<span class="grey" itemprop="bestRating">10</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
5
backend/tests/fixtures/imdb/without-rating.html
vendored
Normal file
5
backend/tests/fixtures/imdb/without-rating.html
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="title_block">
|
||||
<div class="title_bar_wrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
28
backend/tests/fixtures/tmdb/movie/details.json
vendored
28
backend/tests/fixtures/tmdb/movie/details.json
vendored
@ -4,6 +4,10 @@
|
||||
"belongs_to_collection": null,
|
||||
"budget": 160000000,
|
||||
"genres": [
|
||||
{
|
||||
"id": 28,
|
||||
"name": "Action"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Adventure"
|
||||
@ -11,10 +15,6 @@
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Fantasy"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "Action"
|
||||
}
|
||||
],
|
||||
"homepage": "http://www.warcraft-themovie.com/",
|
||||
@ -23,7 +23,7 @@
|
||||
"original_language": "en",
|
||||
"original_title": "Warcraft",
|
||||
"overview": "The peaceful realm of Azeroth stands on the brink of war as its civilization faces a fearsome race of invaders: orc warriors fleeing their dying home to colonize another. As a portal opens to connect the two worlds, one army faces destruction and the other faces extinction. From opposing sides, two heroes are set on a collision course that will decide the fate of their family, their people, and their home.",
|
||||
"popularity": 4.823419,
|
||||
"popularity": 4.131139,
|
||||
"poster_path": "/ckrTPz6FZ35L5ybjqvkLWzzSLO7.jpg",
|
||||
"production_companies": [
|
||||
{
|
||||
@ -62,7 +62,7 @@
|
||||
}
|
||||
],
|
||||
"release_date": "2016-05-25",
|
||||
"revenue": 433537548,
|
||||
"revenue": 433677183,
|
||||
"runtime": 123,
|
||||
"spoken_languages": [
|
||||
{
|
||||
@ -75,5 +75,19 @@
|
||||
"title": "Warcraft",
|
||||
"video": false,
|
||||
"vote_average": 6.2,
|
||||
"vote_count": 1265
|
||||
"vote_count": 1627,
|
||||
"videos": {
|
||||
"results": [
|
||||
{
|
||||
"id": "571cf024c3a3684e98001b34",
|
||||
"iso_639_1": "en",
|
||||
"iso_3166_1": "US",
|
||||
"key": "2Rxoz13Bthc",
|
||||
"name": "Official Trailer",
|
||||
"site": "YouTube",
|
||||
"size": 1080,
|
||||
"type": "Trailer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
24
backend/tests/fixtures/tmdb/movie/movie.json
vendored
24
backend/tests/fixtures/tmdb/movie/movie.json
vendored
@ -11,7 +11,11 @@
|
||||
"id": 68735,
|
||||
"media_type": "movie",
|
||||
"original_language": "en",
|
||||
"title": "Warcraft: The Beginning"
|
||||
"title": "Warcraft: The Beginning",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
},
|
||||
{
|
||||
"poster_path": "/1rxgOwMNwpVP0Hj8R0zhW1CJLfh.jpg",
|
||||
@ -23,7 +27,11 @@
|
||||
"id": 391584,
|
||||
"media_type": "movie",
|
||||
"original_language": "de",
|
||||
"title": "World of Warcraft - Geschichte eines Kult-Spiels"
|
||||
"title": "World of Warcraft - Geschichte eines Kult-Spiels",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
},
|
||||
{
|
||||
"poster_path": "/Y6M5JsoYPrtbTnOY1bCOx3IuDi.jpg",
|
||||
@ -35,7 +43,11 @@
|
||||
"id": 301865,
|
||||
"media_type": "movie",
|
||||
"original_language": "en",
|
||||
"title": "World of Warcraft: Looking For Group"
|
||||
"title": "World of Warcraft: Looking For Group",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
},
|
||||
{
|
||||
"poster_path": "/fivN0U4HXUMXKtyYfi5S8zdhHTg.jpg",
|
||||
@ -45,7 +57,11 @@
|
||||
"id": 205729,
|
||||
"media_type": "movie",
|
||||
"original_language": "en",
|
||||
"title": "World of Warcraft - Cataclysm - Behind the Scenes"
|
||||
"title": "World of Warcraft - Cataclysm - Behind the Scenes",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
}
|
||||
]
|
||||
}
|
41
backend/tests/fixtures/tmdb/tv/details.json
vendored
41
backend/tests/fixtures/tmdb/tv/details.json
vendored
@ -1,5 +1,5 @@
|
||||
{
|
||||
"backdrop_path": "/aKz3lXU71wqdslC1IYRC3yHD6yw.jpg",
|
||||
"backdrop_path": "/mUkuc2wyV9dHLG0D0Loaw5pO2s8.jpg",
|
||||
"created_by": [
|
||||
{
|
||||
"id": 9813,
|
||||
@ -38,12 +38,16 @@
|
||||
"en",
|
||||
"de"
|
||||
],
|
||||
"last_air_date": "2017-06-25",
|
||||
"last_air_date": "2017-07-16",
|
||||
"name": "Game of Thrones",
|
||||
"networks": [
|
||||
{
|
||||
"id": 49,
|
||||
"name": "HBO"
|
||||
},
|
||||
{
|
||||
"id": 1063,
|
||||
"name": "Sky Atlantic"
|
||||
}
|
||||
],
|
||||
"number_of_episodes": 61,
|
||||
@ -54,7 +58,7 @@
|
||||
"original_language": "en",
|
||||
"original_name": "Game of Thrones",
|
||||
"overview": "Seven noble families fight for control of the mythical land of Westeros. Friction between the houses leads to full-scale war. All while a very ancient evil awakens in the farthest north. Amidst the war, a neglected military order of misfits, the Night's Watch, is all that stands between the realms of men and icy horrors beyond.",
|
||||
"popularity": 35.712295,
|
||||
"popularity": 43.231961,
|
||||
"poster_path": "/jIhL6mlT7AblhbHJgEoiBIOUVl1.jpg",
|
||||
"production_companies": [
|
||||
{
|
||||
@ -97,14 +101,14 @@
|
||||
"air_date": "2012-04-01",
|
||||
"episode_count": 10,
|
||||
"id": 3625,
|
||||
"poster_path": "/3U8IVLqitMHMuEAgkuz8qReguHd.jpg",
|
||||
"poster_path": "/5tuhCkqPOT20XPwwi9NhFnC1g9R.jpg",
|
||||
"season_number": 2
|
||||
},
|
||||
{
|
||||
"air_date": "2013-03-31",
|
||||
"episode_count": 10,
|
||||
"id": 3626,
|
||||
"poster_path": "/eVWAat0GqF6s5LLThrI7ClpKr96.jpg",
|
||||
"poster_path": "/7d3vRgbmnrRQ39Qmzd66bQyY7Is.jpg",
|
||||
"season_number": 3
|
||||
},
|
||||
{
|
||||
@ -129,7 +133,7 @@
|
||||
"season_number": 6
|
||||
},
|
||||
{
|
||||
"air_date": "2017-06-25",
|
||||
"air_date": "2017-07-16",
|
||||
"episode_count": 1,
|
||||
"id": 81266,
|
||||
"poster_path": null,
|
||||
@ -138,6 +142,27 @@
|
||||
],
|
||||
"status": "Returning Series",
|
||||
"type": "Scripted",
|
||||
"vote_average": 7.9,
|
||||
"vote_count": 1528
|
||||
"vote_average": 8,
|
||||
"vote_count": 2074,
|
||||
"videos": {
|
||||
"results": [
|
||||
{
|
||||
"id": "572003f992514121f30003c6",
|
||||
"iso_639_1": "en",
|
||||
"iso_3166_1": "US",
|
||||
"key": "BpJYNVhGf1s",
|
||||
"name": "\"The Game Begins\"",
|
||||
"site": "YouTube",
|
||||
"size": 720,
|
||||
"type": "Trailer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"external_ids": {
|
||||
"imdb_id": "tt0944947",
|
||||
"freebase_mid": "/m/0524b41",
|
||||
"freebase_id": "/en/game_of_thrones",
|
||||
"tvdb_id": 121361,
|
||||
"tvrage_id": 24493
|
||||
}
|
||||
}
|
6
backend/tests/fixtures/tmdb/tv/trending.json
vendored
6
backend/tests/fixtures/tmdb/tv/trending.json
vendored
@ -12,7 +12,11 @@
|
||||
18
|
||||
],
|
||||
"name": "Game of Thrones",
|
||||
"original_name": "Game of Thrones"
|
||||
"original_name": "Game of Thrones",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
},
|
||||
{
|
||||
"poster_path": "/3iFm6Kz7iYoFaEcj4fLyZHAmTQA.jpg",
|
||||
|
18
backend/tests/fixtures/tmdb/tv/tv.json
vendored
18
backend/tests/fixtures/tmdb/tv/tv.json
vendored
@ -12,7 +12,11 @@
|
||||
18
|
||||
],
|
||||
"name": "Game of Thrones",
|
||||
"original_name": "Game of Thrones"
|
||||
"original_name": "Game of Thrones",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
},
|
||||
{
|
||||
"poster_path": "/8y1LSfnb8Dfd3XPIrxlpvZ4e8d.jpg",
|
||||
@ -27,7 +31,11 @@
|
||||
"id": 269623,
|
||||
"media_type": "movie",
|
||||
"original_language": "en",
|
||||
"title": "Game of Thrones: Complete History and Lore"
|
||||
"title": "Game of Thrones: Complete History and Lore",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
},
|
||||
{
|
||||
"poster_path": null,
|
||||
@ -39,7 +47,11 @@
|
||||
"id": 340200,
|
||||
"media_type": "movie",
|
||||
"original_language": "en",
|
||||
"title": "Game of Thrones: A Day in the Life"
|
||||
"title": "Game of Thrones: A Day in the Life",
|
||||
"vote_average": 1,
|
||||
"popularity": 1,
|
||||
"backdrop_path": "xxx",
|
||||
"overview": "xxx"
|
||||
}
|
||||
]
|
||||
}
|
25
backend/tests/fixtures/tmdb/videos.json
vendored
Normal file
25
backend/tests/fixtures/tmdb/videos.json
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": 123,
|
||||
"results": [
|
||||
{
|
||||
"id": "57b1f65492514147e0002da5",
|
||||
"iso_639_1": "en",
|
||||
"iso_3166_1": "US",
|
||||
"key": "qnIhJwhBeqY",
|
||||
"name": "Trailer",
|
||||
"site": "YouTube",
|
||||
"size": 480,
|
||||
"type": "Trailer"
|
||||
},
|
||||
{
|
||||
"id": "533ec652c3a3685448000101",
|
||||
"iso_639_1": "en",
|
||||
"iso_3166_1": "US",
|
||||
"key": "6WcJbPlAknw",
|
||||
"name": "The Lord Of The Rings (Extract) 1978",
|
||||
"site": "YouTube",
|
||||
"size": 360,
|
||||
"type": "Clip"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
require('../resources/sass/app.scss');
|
||||
|
||||
import Vue from 'vue';
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
|
||||
import SiteHeader from './components/Header.vue';
|
||||
import Search from './components/Search.vue';
|
||||
|
@ -1,23 +1,17 @@
|
||||
<template>
|
||||
<transition mode="out-in" name="fade">
|
||||
<div class="item-wrap">
|
||||
<div class="item-image-wrap">
|
||||
<span v-if="localItem.rating != null" :class="'item-rating rating-' + localItem.rating" @click="changeRating()">
|
||||
<i class="icon-rating"></i>
|
||||
</span>
|
||||
<span v-if="localItem.rating == null && localItem.tmdb_id && ! rated" class="item-rating item-new" @click="addNewItem()">
|
||||
<i class="icon-add"></i>
|
||||
</span>
|
||||
<span v-if="localItem.rating == null && localItem.tmdb_id && rated" class="item-rating item-new">
|
||||
<span class="loader smallsize-loader"><i></i></span>
|
||||
</span>
|
||||
<div class="item-image-wrap no-select">
|
||||
<rating :item="localItem" :set-item="setItem"></rating>
|
||||
|
||||
<router-link v-if="localItem.tmdb_id" :to="suggestions" class="recommend-item">{{ lang('suggestions') }}</router-link>
|
||||
<span v-if="auth && localItem.rating == null" class="add-to-watchlist" @click="addToWatchlist()">Add to watchlist</span>
|
||||
<span v-if="auth && ! localItem.tmdb_id" class="edit-item" @click="editItem()">Edit</span>
|
||||
|
||||
<span class="remove-item" v-if="localItem.rating != null && auth" @click="removeItem()">{{ lang('delete movie') }}</span>
|
||||
|
||||
<img v-if="localItem.poster" :src="poster" class="item-image" width="185" height="278">
|
||||
<img v-if=" ! localItem.poster" :src="noImage" class="item-image" width="185" height="278">
|
||||
<router-link :to="{ name: `subpage-${localItem.media_type}`, params: { tmdbId: localItem.tmdb_id, slug: localItem.slug }}">
|
||||
<img v-if="localItem.poster" :src="poster" class="item-image" width="185" height="278">
|
||||
<img v-if=" ! localItem.poster" :src="noImage" class="item-image" width="185" height="278">
|
||||
</router-link>
|
||||
|
||||
<span class="show-episode" @click="openSeasonModal()" v-if="displaySeason">
|
||||
<span class="season-item"><i>S</i>{{ season }}</span>
|
||||
@ -28,7 +22,7 @@
|
||||
<div class="item-content">
|
||||
<span v-if="date == 1" class="item-year">{{ released }}</span>
|
||||
<i class="item-has-src" v-if="hasSrc"></i>
|
||||
<a :href="youtube" target="_blank" :title="localItem.title" class="item-title">{{ localItem.title }}</a>
|
||||
<router-link :to="{ name: `subpage-${localItem.media_type}`, params: { tmdbId: localItem.tmdb_id }}" class="item-title" :title="localItem.title">{{ localItem.title }}</router-link>
|
||||
<span v-if="genre == 1" class="item-genre">{{ localItem.genre }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -36,32 +30,24 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Rating from '../Rating.vue';
|
||||
|
||||
import http from 'axios';
|
||||
import debounce from 'debounce';
|
||||
import Helper from '../../helper';
|
||||
|
||||
import { mapMutations, mapActions } from 'vuex';
|
||||
|
||||
const ratingMilliseconds = 700;
|
||||
const newItemMilliseconds = 200;
|
||||
|
||||
export default {
|
||||
mixins: [Helper],
|
||||
|
||||
props: ['item', 'genre', 'date'],
|
||||
|
||||
created() {
|
||||
this.saveNewRating = debounce(this.saveNewRating, ratingMilliseconds);
|
||||
this.addNewItem = debounce(this.addNewItem, newItemMilliseconds, true);
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localItem: this.item,
|
||||
latestEpisode: this.item.latest_episode,
|
||||
auth: config.auth,
|
||||
prevRating: null,
|
||||
rated: false
|
||||
auth: config.auth
|
||||
}
|
||||
},
|
||||
|
||||
@ -79,7 +65,9 @@
|
||||
},
|
||||
|
||||
suggestions() {
|
||||
return `/suggestions?for=${this.localItem.tmdb_id}&name=${this.localItem.title}&type=${this.localItem.media_type}`;
|
||||
const item = this.localItem;
|
||||
|
||||
return `/suggestions?for=${item.tmdb_id}&name=${item.title}&type=${item.media_type}`;
|
||||
},
|
||||
|
||||
noImage() {
|
||||
@ -97,10 +85,6 @@
|
||||
return released.getFullYear();
|
||||
},
|
||||
|
||||
youtube() {
|
||||
return `https://www.youtube.com/results?search_query=${this.localItem.title} ${this.released} Trailer`;
|
||||
},
|
||||
|
||||
displaySeason() {
|
||||
return this.localItem.media_type == 'tv' && this.localItem.rating != null && this.localItem.tmdb_id;
|
||||
},
|
||||
@ -140,50 +124,21 @@
|
||||
});
|
||||
},
|
||||
|
||||
changeRating() {
|
||||
if(this.auth) {
|
||||
this.prevRating = this.localItem.rating;
|
||||
this.localItem.rating = this.prevRating == 3 ? 1 : +this.prevRating + 1;
|
||||
|
||||
this.saveNewRating();
|
||||
}
|
||||
setItem(item) {
|
||||
this.localItem = item;
|
||||
},
|
||||
|
||||
saveNewRating() {
|
||||
http.patch(`${config.api}/change-rating/${this.localItem.id}`, {rating: this.localItem.rating}).catch(error => {
|
||||
this.localItem.rating = this.prevRating;
|
||||
alert('Error in saveNewRating()');
|
||||
});
|
||||
addToWatchlist() {
|
||||
|
||||
},
|
||||
|
||||
addNewItem() {
|
||||
if(this.auth) {
|
||||
this.rated = true;
|
||||
editItem() {
|
||||
|
||||
http.post(`${config.api}/add`, {item: this.localItem}).then(value => {
|
||||
this.localItem = value.data;
|
||||
this.rated = false;
|
||||
}, error => {
|
||||
if(error.status == 409) {
|
||||
alert(this.localItem.title + ' already exists!');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeItem() {
|
||||
if(this.auth) {
|
||||
const confirm = window.confirm(this.lang('confirm delete'));
|
||||
|
||||
if(confirm) {
|
||||
http.delete(`${config.api}/remove/${this.localItem.id}`).then(value => {
|
||||
this.localItem.rating = null;
|
||||
}, error => {
|
||||
alert('Error in removeItem()');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
Rating
|
||||
}
|
||||
}
|
||||
</script>
|
221
client/app/components/Content/Subpage.vue
Normal file
221
client/app/components/Content/Subpage.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<main>
|
||||
<!-- todo: make header position absolute, and float teaser and content correct -->
|
||||
<section class="big-teaser-wrap" :class="{active: itemLoadedSubpage}" v-show=" ! loading">
|
||||
|
||||
<div class="big-teaser-image" :style="backdropImage"></div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="big-teaser-content">
|
||||
<div class="big-teaser-data-wrap">
|
||||
|
||||
<div class="subpage-poster-wrap-mobile">
|
||||
<rating :item="item" :set-item="setItem"></rating>
|
||||
<img class="base" :src="noImage" width="120" height="180">
|
||||
<img class="real" :src="posterImage" width="120" height="180">
|
||||
</div>
|
||||
|
||||
<!-- todo: move to own component -->
|
||||
<div class="big-teaser-item-data">
|
||||
<span class="item-year">{{ released }}</span>
|
||||
<span class="item-title">{{ item.title }}</span>
|
||||
<span class="item-genre">{{ item.genre }}</span>
|
||||
</div>
|
||||
<div class="big-teaser-buttons no-select" :class="{'without-watchlist': item.rating != null || ! auth}">
|
||||
<span @click="openTrailer()" v-if="item.youtube_key" class="button-trailer"><i class="icon-trailer"></i> Watch Trailer</span>
|
||||
<span v-if="item.rating == null && auth" class="button-watchlist"><i class="icon-watchlist"></i> Add to Watchlist</span>
|
||||
<a :href="`https://www.themoviedb.org/${item.media_type}/${item.tmdb_id}`" target="_blank" class="button-tmdb-rating">
|
||||
<i v-if="item.tmdb_rating && item.tmdb_rating != 0"><b>{{ item.tmdb_rating }}</b> TMDb</i>
|
||||
<i v-else>No TMDb Rating</i>
|
||||
</a>
|
||||
<a v-if="item.imdb_id" :href="`http://www.imdb.com/title/${item.imdb_id}`" target="_blank" class="button-imdb-rating">
|
||||
<i v-if="loadingImdb">Loading IMDb Rating...</i>
|
||||
<i v-if="item.imdb_rating && ! loadingImdb"><b>{{ item.imdb_rating }}</b> IMDb</i>
|
||||
<i v-if=" ! item.imdb_rating && ! loadingImdb">No IMDb Rating</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="subpage-content" :class="{active: itemLoadedSubpage}" v-show=" ! loading">
|
||||
<div class="wrap">
|
||||
<div class="subpage-overview">
|
||||
<h2>Overview</h2>
|
||||
<p>{{ overview }}</p>
|
||||
</div>
|
||||
|
||||
<div class="subpage-sidebar">
|
||||
<div class="subpage-poster-wrap">
|
||||
<rating :item="item" :set-item="setItem"></rating>
|
||||
<img class="base" :src="noImage" width="272" height="408">
|
||||
<img class="real" :src="posterImage" width="272" height="408">
|
||||
</div>
|
||||
|
||||
<!-- todo: move to own component -->
|
||||
<div class="subpage-sidebar-buttons no-select" v-if="item.rating != null && auth">
|
||||
<span class="edit-data">Edit data</span>
|
||||
<span class="remove-item" @click="removeItem()">{{ lang('delete movie') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="loader fullsize-loader" v-show="loading"><i></i></span>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Rating from '../Rating.vue';
|
||||
import { mapMutations, mapState } from 'vuex'
|
||||
import Helper from '../../helper';
|
||||
|
||||
import http from 'axios';
|
||||
|
||||
export default {
|
||||
mixins: [Helper],
|
||||
|
||||
props: ['mediaType'],
|
||||
|
||||
created() {
|
||||
document.body.classList.add('subpage-open');
|
||||
window.scrollTo(0, 0);
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
document.body.classList.remove('subpage-open');
|
||||
this.SET_ITEM_LOADED_SUBPAGE(false);
|
||||
this.CLOSE_MODAL();
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
item: {},
|
||||
loadingImdb: false,
|
||||
auth: config.auth
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
loading: state => state.loading,
|
||||
itemLoadedSubpage: state => state.itemLoadedSubpage
|
||||
}),
|
||||
|
||||
overview() {
|
||||
return this.item.overview ? this.item.overview : '-';
|
||||
},
|
||||
|
||||
backdropImage() {
|
||||
let backdropUrl = config.backdropTMDB;
|
||||
|
||||
if(this.item.rating != null) {
|
||||
backdropUrl = config.backdrop;
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundImage: `url(${backdropUrl}${this.item.backdrop})`
|
||||
}
|
||||
},
|
||||
|
||||
posterImage() {
|
||||
if(this.item.rating != null) {
|
||||
return config.posterSubpage + this.item.poster;
|
||||
}
|
||||
|
||||
return config.posterSubpageTMDB + this.item.poster;
|
||||
},
|
||||
|
||||
released() {
|
||||
const released = new Date(this.item.released * 1000);
|
||||
|
||||
return released.getFullYear();
|
||||
},
|
||||
|
||||
noImage() {
|
||||
return config.url + '/assets/img/no-image-subpage.png';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations([ 'SET_LOADING', 'SET_ITEM_LOADED_SUBPAGE', 'OPEN_MODAL', 'CLOSE_MODAL' ]),
|
||||
|
||||
openTrailer() {
|
||||
this.OPEN_MODAL({
|
||||
type: 'trailer',
|
||||
data: {
|
||||
youtubeKey: this.item.youtube_key,
|
||||
title: this.item.title
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchImdbRating() {
|
||||
if(this.item.imdb_id && this.item.rating == null) {
|
||||
this.loadingImdb = true;
|
||||
|
||||
http(`${config.api}/imdb-rating/${this.item.imdb_id}`).then(response => {
|
||||
const rating = this.intToFloat(response.data);
|
||||
|
||||
this.$set(this.item, 'imdb_rating', rating);
|
||||
this.loadingImdb = false;
|
||||
}, error => {
|
||||
alert(error);
|
||||
this.loadingImdb = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchData() {
|
||||
const tmdbId = this.$route.params.tmdbId;
|
||||
|
||||
this.SET_LOADING(true);
|
||||
http(`${config.api}/item/${tmdbId}/${this.mediaType}`).then(response => {
|
||||
this.item = response.data;
|
||||
this.item.tmdb_rating = this.intToFloat(response.data.tmdb_rating);
|
||||
|
||||
this.disableLoading();
|
||||
this.fetchImdbRating();
|
||||
}, error => {
|
||||
alert(error);
|
||||
this.SET_LOADING(false);
|
||||
});
|
||||
},
|
||||
|
||||
disableLoading() {
|
||||
setTimeout(() => {
|
||||
this.SET_LOADING(false);
|
||||
this.displayItem();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
displayItem() {
|
||||
setTimeout(() => {
|
||||
this.SET_ITEM_LOADED_SUBPAGE(true);
|
||||
}, 50);
|
||||
},
|
||||
|
||||
setItem(item) {
|
||||
this.item = item;
|
||||
},
|
||||
|
||||
removeItem() {
|
||||
const confirm = window.confirm(this.lang('confirm delete'));
|
||||
|
||||
if(confirm) {
|
||||
http.delete(`${config.api}/remove/${this.item.id}`).then(response => {
|
||||
this.item.rating = null;
|
||||
}, error => {
|
||||
alert(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
Rating
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<header>
|
||||
<header :class="{active: displayHeader}">
|
||||
<div class="wrap">
|
||||
<router-link to="/" class="logo" >
|
||||
<img src="../../../public/assets/img/logo.png" alt="Flox" width="108" height="32">
|
||||
</router-link>
|
||||
|
||||
<span class="sort-wrap">
|
||||
<span class="sort-wrap" v-if=" ! isSubpage()">
|
||||
<i :title="lang('last seen')" class="icon-sort-time" :class="{active: userFilter == 'last_seen_at'}" @click="setUserFilter('last_seen_at')"></i>
|
||||
<i :title="lang('best rated')" class="icon-sort-star" :class="{active: userFilter == 'rating'}" @click="setUserFilter('rating')"></i>
|
||||
<!-- will be moved into footer -->
|
||||
<span :title="lang('change color')" class="icon-constrast" @click="toggleColorScheme()"><i></i></span>
|
||||
</span>
|
||||
|
||||
@ -17,8 +18,8 @@
|
||||
</ul>
|
||||
|
||||
<ul class="site-nav-second">
|
||||
<li><router-link to="/tv">{{ lang('tv') }}</router-link></li>
|
||||
<li><router-link to="/movies">{{ lang('movies') }}</router-link></li>
|
||||
<li><router-link to="/tv" exact>{{ lang('tv') }}</router-link></li>
|
||||
<li><router-link to="/movies" exact>{{ lang('movies') }}</router-link></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@ -40,8 +41,10 @@
|
||||
computed: {
|
||||
...mapState({
|
||||
userFilter: state => state.userFilter,
|
||||
colorScheme: state => state.colorScheme
|
||||
colorScheme: state => state.colorScheme,
|
||||
itemLoadedSubpage: state => state.itemLoadedSubpage
|
||||
}),
|
||||
|
||||
root() {
|
||||
return config.uri;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
<div class="all-modals">
|
||||
<transition mode="out-in" name="fade">
|
||||
<season v-if="modalType == 'season'"></season>
|
||||
<trailer v-if="modalType == 'trailer'"></trailer>
|
||||
</transition>
|
||||
<span class="overlay" v-if="overlay" @click="CLOSE_MODAL()"></span>
|
||||
</div>
|
||||
@ -9,6 +10,7 @@
|
||||
|
||||
<script>
|
||||
import Season from './Season.vue';
|
||||
import Trailer from './Trailer.vue';
|
||||
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
|
||||
@ -25,7 +27,7 @@
|
||||
},
|
||||
|
||||
components: {
|
||||
Season
|
||||
Season, Trailer
|
||||
}
|
||||
}
|
||||
</script>
|
40
client/app/components/Modal/Trailer.vue
Normal file
40
client/app/components/Modal/Trailer.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="modal-wrap modal-wrap-big">
|
||||
|
||||
<div class="modal-header">
|
||||
<span>Trailer for {{ modalData.title }}</span>
|
||||
<span class="close-modal" @click="CLOSE_MODAL()">
|
||||
<i class="icon-close"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<iframe width="100%" height="100%" :src="trailerSrc" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
|
||||
import Helper from '../../helper';
|
||||
|
||||
export default {
|
||||
mixins: [Helper],
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
modalData: state => state.modalData
|
||||
}),
|
||||
|
||||
trailerSrc() {
|
||||
return `https://www.youtube.com/embed/${this.modalData.youtubeKey}`;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations([ 'CLOSE_MODAL' ])
|
||||
}
|
||||
}
|
||||
</script>
|
70
client/app/components/Rating.vue
Normal file
70
client/app/components/Rating.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="item.rating != null" :class="'item-rating rating-' + item.rating" @click="changeRating()">
|
||||
<i class="icon-rating"></i>
|
||||
</span>
|
||||
<span v-if="item.rating == null && item.tmdb_id && ! rated && auth" class="item-rating item-new" @click="addNewItem()">
|
||||
<i class="icon-add"></i>
|
||||
</span>
|
||||
<span v-if="item.rating == null && item.tmdb_id && rated" class="item-rating item-new">
|
||||
<span class="loader smallsize-loader"><i></i></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'debounce';
|
||||
import http from 'axios';
|
||||
|
||||
const ratingMilliseconds = 700;
|
||||
const newItemMilliseconds = 200;
|
||||
|
||||
export default {
|
||||
props: ['item', 'set-item'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
rated: false,
|
||||
auth: config.auth
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.saveNewRating = debounce(this.saveNewRating, ratingMilliseconds);
|
||||
this.addNewItem = debounce(this.addNewItem, newItemMilliseconds, true);
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeRating() {
|
||||
if(this.auth) {
|
||||
this.prevRating = this.item.rating;
|
||||
this.item.rating = this.prevRating == 3 ? 1 : +this.prevRating + 1;
|
||||
|
||||
this.saveNewRating();
|
||||
}
|
||||
},
|
||||
|
||||
saveNewRating() {
|
||||
http.patch(`${config.api}/change-rating/${this.item.id}`, {rating: this.item.rating}).catch(error => {
|
||||
this.item.rating = this.prevRating;
|
||||
alert('Error in saveNewRating()');
|
||||
});
|
||||
},
|
||||
|
||||
addNewItem() {
|
||||
if(this.auth) {
|
||||
this.rated = true;
|
||||
|
||||
http.post(`${config.api}/add`, {item: this.item}).then(value => {
|
||||
this.setItem(value.data);
|
||||
this.rated = false;
|
||||
}, error => {
|
||||
if(error.status == 409) {
|
||||
alert(this.item.title + ' already exists!');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<section class="search-wrap" :class="{sticky: sticky}">
|
||||
<section class="search-wrap" :class="{sticky: sticky, active: displayHeader}">
|
||||
<div class="wrap">
|
||||
|
||||
<form class="search-form" @submit.prevent="search()">
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
<div class="suggestions-for" v-if="suggestionsFor">
|
||||
<div class="wrap">
|
||||
{{ lang('suggestions for') }} <span>{{ suggestionsFor }}</span>
|
||||
{{ lang('suggestions for') }} <router-link :to="{ name: `subpage-${$route.query.type}`, params: { tmdbId: $route.query.for }}">{{ suggestionsFor }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
<script>
|
||||
import Helper from '../helper';
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
mixins: [Helper],
|
||||
@ -34,6 +35,10 @@
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
itemLoadedSubpage: state => state.itemLoadedSubpage
|
||||
}),
|
||||
|
||||
suggestionsFor() {
|
||||
return this.$route.query.name;
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
import http from 'axios';
|
||||
http.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('#token').getAttribute('content');
|
||||
|
||||
const {url, uri, auth, language} = document.body.dataset;
|
||||
const {url, uri, auth, language, posterTmdb, posterSubpageTmdb, backdropTmdb} = document.body.dataset;
|
||||
|
||||
const config = {
|
||||
uri,
|
||||
@ -9,7 +9,11 @@ const config = {
|
||||
auth,
|
||||
language,
|
||||
poster: url + '/assets/poster',
|
||||
posterTMDB: 'http://image.tmdb.org/t/p/w185',
|
||||
backdrop: url + '/assets/backdrop',
|
||||
posterSubpage: url + '/assets/poster/subpage',
|
||||
posterTMDB: posterTmdb,
|
||||
posterSubpageTMDB: posterSubpageTmdb,
|
||||
backdropTMDB: backdropTmdb,
|
||||
api: url + '/api'
|
||||
};
|
||||
|
||||
|
@ -28,6 +28,14 @@ export default {
|
||||
return item;
|
||||
},
|
||||
|
||||
intToFloat(int) {
|
||||
if(int) {
|
||||
return parseFloat(int).toFixed(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
// Language helper
|
||||
lang(text) {
|
||||
const language = JSON.parse(config.language);
|
||||
@ -43,6 +51,20 @@ export default {
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
isSubpage() {
|
||||
return this.$route.name.includes('subpage');
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayHeader() {
|
||||
if(this.isSubpage()) {
|
||||
return this.itemLoadedSubpage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import Content from './components/Content/Content.vue';
|
||||
import SearchContent from './components/Content/SearchContent.vue';
|
||||
import Settings from './components/Content/Settings/Index.vue';
|
||||
import TMDBContent from './components/Content/TMDBContent.vue';
|
||||
import Subpage from './components/Content/Subpage.vue';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
@ -15,13 +16,20 @@ export default new Router({
|
||||
base: config.uri,
|
||||
routes: [
|
||||
{ path: '/', component: Content, name: 'home' },
|
||||
|
||||
// todo: use props for media type
|
||||
{ path: '/movies', component: Content, name: 'movie' },
|
||||
{ path: '/tv', component: Content, name: 'tv' },
|
||||
|
||||
{ path: '/movies/:tmdbId/:slug?', component: Subpage, name: 'subpage-movie', props: {mediaType: 'movie'} },
|
||||
{ path: '/tv/:tmdbId/:slug?', component: Subpage, name: 'subpage-tv', props: {mediaType: 'tv'} },
|
||||
|
||||
{ path: '/search', component: SearchContent, name: 'search' },
|
||||
{ path: '/settings', component: Settings, name: 'settings' },
|
||||
{ path: '/suggestions', component: TMDBContent, name: 'suggestions' },
|
||||
{ path: '/trending', component: TMDBContent, name: 'trending' },
|
||||
{ path: '/upcoming', component: TMDBContent, name: 'upcoming' },
|
||||
|
||||
{ path: '*', component: Content }
|
||||
]
|
||||
});
|
@ -37,7 +37,11 @@ export function setSearchTitle({commit}, title) {
|
||||
}
|
||||
|
||||
export function setColorScheme({commit}, color) {
|
||||
document.body.classList.remove('dark', 'light');
|
||||
|
||||
localStorage.setItem('color', color);
|
||||
document.body.classList.add(color);
|
||||
|
||||
commit('SET_COLOR_SCHEME', color);
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,8 @@ export default new Vuex.Store({
|
||||
modalData: {},
|
||||
loadingModalData: true,
|
||||
seasonActiveModal: 1,
|
||||
modalType: ''
|
||||
modalType: '',
|
||||
itemLoadedSubpage: false
|
||||
},
|
||||
mutations,
|
||||
actions
|
||||
|
@ -57,5 +57,9 @@ export default {
|
||||
|
||||
[type.SET_MODAL_DATA](state, data) {
|
||||
state.modalData = data;
|
||||
},
|
||||
|
||||
[type.SET_ITEM_LOADED_SUBPAGE](state, bool) {
|
||||
state.itemLoadedSubpage = bool;
|
||||
}
|
||||
}
|
@ -11,3 +11,4 @@ export const OPEN_MODAL = 'OPEN_MODAL';
|
||||
export const SET_SEASON_ACTIVE_MODAL = 'SET_SEASON_ACTIVE_MODAL';
|
||||
export const SET_LOADING_MODAL_DATA = 'SET_LOADING_MODAL_DATA';
|
||||
export const SET_MODAL_DATA = 'SET_MODAL_DATA';
|
||||
export const SET_ITEM_LOADED_SUBPAGE = 'SET_ITEM_LOADED_SUBPAGE';
|
||||
|
@ -13,7 +13,7 @@
|
||||
"vuex": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.5.0",
|
||||
"autoprefixer": "^6.7.6",
|
||||
"babel-core": "^6.17.0",
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
|
@ -14,12 +14,15 @@
|
||||
<body
|
||||
data-url="{{ url('/') }}"
|
||||
data-uri="{{ config('app.CLIENT_URI') }}"
|
||||
data-poster-tmdb="{{ config('services.tmdb.poster') }}"
|
||||
data-poster-subpage-tmdb="{{ config('services.tmdb.poster_subpage') }}"
|
||||
data-backdrop-tmdb="{{ config('services.tmdb.backdrop') }}"
|
||||
data-auth="{{ Auth::check() }}"
|
||||
data-language="{{ $lang }}"
|
||||
class="{{ Auth::check() ? 'logged' : 'guest' }}"
|
||||
>
|
||||
|
||||
<div id="app" :class="colorScheme">
|
||||
<div id="app">
|
||||
@if(Request::is('login'))
|
||||
<login></login>
|
||||
@else
|
||||
|
3
client/resources/sass/_base.scss
vendored
3
client/resources/sass/_base.scss
vendored
@ -1,6 +1,8 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -27,6 +29,7 @@ input {
|
||||
.wrap,
|
||||
.wrap-content {
|
||||
lost-center: 1300px 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrap-content {
|
||||
|
1
client/resources/sass/app.scss
vendored
1
client/resources/sass/app.scss
vendored
@ -9,6 +9,7 @@
|
||||
'components/header',
|
||||
'components/search',
|
||||
'components/content',
|
||||
'components/subpage',
|
||||
'components/login',
|
||||
'components/footer',
|
||||
'components/modal';
|
172
client/resources/sass/components/_content.scss
vendored
172
client/resources/sass/components/_content.scss
vendored
@ -19,6 +19,14 @@ main {
|
||||
@include media(5) {
|
||||
padding: 80px 0 0 0;
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
padding: 0;
|
||||
//padding: 65vh 0 0 0;
|
||||
//position: absolute;
|
||||
//top: 0;
|
||||
//left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestions-for {
|
||||
@ -38,8 +46,13 @@ main {
|
||||
border-top: 1px solid #474747;
|
||||
}
|
||||
|
||||
span {
|
||||
a {
|
||||
color: $main2;
|
||||
text-decoration: none;
|
||||
|
||||
&:active {
|
||||
color: $main1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,10 +88,6 @@ main {
|
||||
.item-new {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.show-episode {
|
||||
bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.show-episode {
|
||||
@ -86,25 +95,21 @@ main {
|
||||
}
|
||||
|
||||
.recommend-item,
|
||||
.remove-item {
|
||||
.add-to-watchlist {
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.item-rating {
|
||||
.logged & {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
|
||||
@include media(3) {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-image {
|
||||
box-shadow: 0 12px 15px 0 rgba(0, 0, 0, .5);
|
||||
|
||||
//@include transition(box-shadow);
|
||||
|
||||
&:hover {
|
||||
//box-shadow: 0 15px 20px 0 rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
@ -127,59 +132,59 @@ main {
|
||||
@include media(5) {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-year,
|
||||
.item-genre {
|
||||
float: left;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
clear: both;
|
||||
margin: 0 5px 0 0;
|
||||
.item-year,
|
||||
.item-genre {
|
||||
float: left;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
clear: both;
|
||||
margin: 0 5px 0 0;
|
||||
|
||||
.dark & {
|
||||
color: #626262;
|
||||
.dark & {
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-has-src {
|
||||
float: left;
|
||||
margin: 5px 0 0 0;
|
||||
font-style: normal;
|
||||
width: 12px;
|
||||
height: 9px;
|
||||
background: url(../../../public/assets/img/has-src.png);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
color: $dark;
|
||||
clear: both;
|
||||
font-size: 17px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
text-decoration: none;
|
||||
float: left;
|
||||
|
||||
.dark & {
|
||||
color: #717171;
|
||||
.item-has-src {
|
||||
float: left;
|
||||
margin: 5px 0 0 0;
|
||||
font-style: normal;
|
||||
width: 12px;
|
||||
height: 9px;
|
||||
background: url(../../../public/assets/img/has-src.png);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $main2;
|
||||
}
|
||||
.item-title {
|
||||
color: $dark;
|
||||
clear: both;
|
||||
font-size: 17px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
text-decoration: none;
|
||||
float: left;
|
||||
|
||||
&:active {
|
||||
color: $main1;
|
||||
}
|
||||
.dark & {
|
||||
color: #717171;
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
font-size: 15px;
|
||||
&:hover {
|
||||
color: $main2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: $main1;
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,10 +197,19 @@ main {
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, .7);
|
||||
border-radius: 25px;
|
||||
z-index: 120;
|
||||
|
||||
.logged & {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
|
||||
@include media(3) {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(-50%, -50%) scale(1.1) !important;
|
||||
}
|
||||
@ -261,6 +275,10 @@ main {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-add {
|
||||
@ -295,41 +313,57 @@ main {
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
font-size: 12px;
|
||||
display: none;
|
||||
/*font-size: 12px;
|
||||
padding: 8px 1px;
|
||||
position: static;
|
||||
float: left;
|
||||
opacity: 1 !important;
|
||||
opacity: 1 !important;*/
|
||||
}
|
||||
}
|
||||
|
||||
.remove-item {
|
||||
.add-to-watchlist,
|
||||
.edit-item {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
background: $rating3;
|
||||
padding: 3px 0;
|
||||
background: #238cce;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
|
||||
@include transition(opacity);
|
||||
@include transition(background, opacity);
|
||||
|
||||
&:hover {
|
||||
background: lighten(#238cce, 5%);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #238cce;
|
||||
opacity: .7 !important;
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
// opacity: 1;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-item {
|
||||
background: $rating2;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
background: lighten($rating2, 5%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $rating2;
|
||||
}
|
||||
}
|
||||
|
||||
|
19
client/resources/sass/components/_header.scss
vendored
19
client/resources/sass/components/_header.scss
vendored
@ -3,22 +3,31 @@ header {
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
width: 100%;
|
||||
padding: 25px 0;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
opacity: 0;
|
||||
|
||||
@include media(5) {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.open-modal & {
|
||||
padding: 25px 16px 25px 0;
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
transition: opacity .7s ease .1s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
float: left;
|
||||
opacity: .9;
|
||||
|
||||
@include media(5) {
|
||||
width: 80px;
|
||||
@ -36,6 +45,7 @@ header {
|
||||
float: right;
|
||||
margin: 4px 0 0 0;
|
||||
width: auto;
|
||||
opacity: .9;
|
||||
|
||||
.icon-sort-time,
|
||||
.icon-sort-star {
|
||||
@ -101,6 +111,7 @@ header {
|
||||
margin: 7px 0 0 40px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
opacity: .9;
|
||||
|
||||
@include media(4) {
|
||||
clear: left;
|
||||
|
22
client/resources/sass/components/_modal.scss
vendored
22
client/resources/sass/components/_modal.scss
vendored
@ -5,7 +5,7 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 50;
|
||||
z-index: 250;
|
||||
}
|
||||
|
||||
.modal-wrap {
|
||||
@ -16,13 +16,31 @@
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
box-shadow: 0 5px 20px 0 rgba(#000, .6);
|
||||
z-index: 100;
|
||||
z-index: 300;
|
||||
|
||||
@include media(4) {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-wrap-big {
|
||||
max-width: 1300px;
|
||||
height: 80%;
|
||||
|
||||
.modal-content {
|
||||
max-height: calc(100% - 45px);
|
||||
height: 100%;
|
||||
|
||||
iframe {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
@include media(4) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
|
36
client/resources/sass/components/_search.scss
vendored
36
client/resources/sass/components/_search.scss
vendored
@ -4,7 +4,8 @@
|
||||
position: absolute;
|
||||
box-shadow: 0 0 70px 0 rgba(0, 0, 0, .3);
|
||||
background: #fff;
|
||||
opacity: .97;
|
||||
z-index: 200;
|
||||
opacity: 0;
|
||||
|
||||
.dark & {
|
||||
background: #2f2f2f;
|
||||
@ -19,9 +20,21 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
position: absolute;
|
||||
top: auto;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
&.active {
|
||||
transition: opacity .7s ease .1s;
|
||||
opacity: .97;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
@ -34,6 +47,10 @@
|
||||
.sticky & {
|
||||
padding: 0 0 0 50px;
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
@ -52,6 +69,10 @@
|
||||
@include media(5) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-search {
|
||||
@ -61,6 +82,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 15px;
|
||||
opacity: .5;
|
||||
|
||||
@include transition(left);
|
||||
|
||||
@ -71,6 +93,12 @@
|
||||
.sticky & {
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
background: url(../../../public/assets/img/search-dark.png);
|
||||
opacity: 1;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-logo-small {
|
||||
@ -96,4 +124,8 @@
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-open & {
|
||||
display: none;
|
||||
}
|
||||
}
|
440
client/resources/sass/components/_subpage.scss
vendored
Normal file
440
client/resources/sass/components/_subpage.scss
vendored
Normal file
@ -0,0 +1,440 @@
|
||||
.big-teaser-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
width: 100%;
|
||||
//height: 65vh;
|
||||
height: 600px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 70px 0 rgba(0,0,0,.5);
|
||||
|
||||
.open-modal & {
|
||||
padding: 0 20px 0 0;
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
@include media(4) {
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
@include media(6) {
|
||||
height: 550px;
|
||||
}
|
||||
}
|
||||
|
||||
.button-trailer {
|
||||
background: $dark;
|
||||
color: rgba(255,255,255,.9);
|
||||
|
||||
&:hover {
|
||||
background: lighten($dark, 5%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $dark;
|
||||
}
|
||||
|
||||
@include media(4) {
|
||||
.without-watchlist & {
|
||||
//width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-watchlist {
|
||||
background: #238cce;
|
||||
color: rgba(255,255,255,.9);
|
||||
|
||||
&:hover {
|
||||
background: lighten(#238cce, 5%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #238cce;
|
||||
}
|
||||
}
|
||||
|
||||
.button-tmdb-rating {
|
||||
background: #00d373;
|
||||
color: $dark;
|
||||
|
||||
i {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: lighten(#00d373, 5%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #00d373;
|
||||
}
|
||||
}
|
||||
|
||||
.button-imdb-rating {
|
||||
background: #e3b81f;
|
||||
color: $dark;
|
||||
|
||||
i {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: lighten(#e3b81f, 10%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #e3b81f;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-trailer {
|
||||
background: url(../../../public/assets/img/trailer.png);
|
||||
width: 7px;
|
||||
height: 8px;
|
||||
float: left;
|
||||
margin: 9px 7px 0 0;
|
||||
|
||||
@include media(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-watchlist {
|
||||
background: url(../../../public/assets/img/watchlist.png);
|
||||
width: 14px;
|
||||
height: 11px;
|
||||
float: left;
|
||||
margin: 7px 7px 0 0;
|
||||
|
||||
@include media(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.big-teaser-buttons {
|
||||
float: left;
|
||||
clear: both;
|
||||
margin: 0 0 0 272px;
|
||||
|
||||
span,
|
||||
a {
|
||||
float: left;
|
||||
padding: 6px 14px;
|
||||
font-size: 17px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
@include transition(background);
|
||||
|
||||
@include media(4) {
|
||||
padding: 5px 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@include media(5) {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@include media(6) {
|
||||
//width: 50%;
|
||||
//overflow: hidden;
|
||||
//text-overflow: ellipsis;
|
||||
//white-space: nowrap;
|
||||
//text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
margin: 0 0 0 200px;
|
||||
}
|
||||
|
||||
@include media(5) {
|
||||
margin: 0 0 0 -20px;
|
||||
}
|
||||
}
|
||||
|
||||
.big-teaser-item-data {
|
||||
float: left;
|
||||
margin: 0 0 40px 300px;
|
||||
|
||||
.item-year,
|
||||
.item-title,
|
||||
.item-genre {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
color: #fff;
|
||||
font-size: 34px;
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
@include media(4) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-year,
|
||||
.item-genre {
|
||||
color: rgba(#fff, .8);
|
||||
|
||||
@include media(4) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media(3) {
|
||||
margin: 0 0 40px 230px;
|
||||
}
|
||||
|
||||
@include media(5) {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.big-teaser-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: 100% 25%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 1s ease 0s;
|
||||
|
||||
.active & {
|
||||
opacity: .2;
|
||||
}
|
||||
}
|
||||
|
||||
.big-teaser-data-wrap {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.big-teaser-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.subpage-content {
|
||||
width: 100%;
|
||||
//padding: calc(65vh - 90px) 0 0 0;
|
||||
padding: 520px 0 0 0;
|
||||
|
||||
@include media(6) {
|
||||
padding: 300px 0 0 0;
|
||||
}
|
||||
|
||||
.open-modal & {
|
||||
padding: 520px 20px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-sidebar {
|
||||
float: left;
|
||||
margin: -330px 0 0 0;
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
|
||||
@include media(3) {
|
||||
margin: -360px 0 0 0;
|
||||
}
|
||||
|
||||
@include media(4) {
|
||||
margin: -450px 0 0 0;
|
||||
}
|
||||
|
||||
@include media(5) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@include media(6) {
|
||||
width: 100%;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-poster-wrap-mobile {
|
||||
float: left;
|
||||
position: relative;
|
||||
display: none;
|
||||
|
||||
@include media(5) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// todo: remove
|
||||
.base {
|
||||
box-shadow: 0 20px 20px 0 rgba(0, 0, 0, .3);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.real {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease 0s;
|
||||
|
||||
.active & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-poster-wrap {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
|
||||
@include media(3) {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.base {
|
||||
box-shadow: 0 20px 20px 0 rgba(0, 0, 0, .3);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.real {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease 0s;
|
||||
|
||||
.active & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include media(5) {
|
||||
//max-width: 100px;
|
||||
//height: auto;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-sidebar-buttons {
|
||||
float: left;
|
||||
clear: both;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
margin: 30px 0 0 0;
|
||||
|
||||
span {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@include media(6) {
|
||||
//clear: none;
|
||||
//width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-item,
|
||||
.refresh-data,
|
||||
.edit-data {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
color: $dark;
|
||||
|
||||
.dark & {
|
||||
color: #717171;
|
||||
}
|
||||
|
||||
@include transition(color);
|
||||
|
||||
&:hover {
|
||||
color: $rating3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-data {
|
||||
|
||||
&:hover {
|
||||
color: #238cce;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-data {
|
||||
|
||||
&:hover {
|
||||
color: $rating2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-overview {
|
||||
width: calc(100% - 310px);
|
||||
float: right;
|
||||
//margin: 0 0 0 30px;
|
||||
padding: 0 0 30px 0;
|
||||
|
||||
@include media(6) {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
float: left;
|
||||
color: $main2;
|
||||
text-transform: uppercase;
|
||||
font-size: 18px;
|
||||
margin: 30px 0 10px 0;
|
||||
|
||||
@include media(6) {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
float: left;
|
||||
clear: both;
|
||||
color: $dark;
|
||||
font-size: 15px;
|
||||
line-height: 19pt;
|
||||
|
||||
@include media(6) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
color: #717171;
|
||||
}
|
||||
}
|
||||
}
|
2
public/assets/app.css
vendored
2
public/assets/app.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
0
public/assets/backdrop/.gitkeep
Normal file
0
public/assets/backdrop/.gitkeep
Normal file
BIN
public/assets/img/no-image-subpage.png
Normal file
BIN
public/assets/img/no-image-subpage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 B |
Binary file not shown.
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 280 B |
BIN
public/assets/img/trailer.png
Normal file
BIN
public/assets/img/trailer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 B |
BIN
public/assets/img/watchlist.png
Normal file
BIN
public/assets/img/watchlist.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 253 B |
0
public/assets/poster/.gitkeep
Normal file
0
public/assets/poster/.gitkeep
Normal file
0
public/assets/poster/subpage/.gitkeep
Normal file
0
public/assets/poster/subpage/.gitkeep
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user