mirror of
https://github.com/devfake/flox.git
synced 2024-11-23 18:42:30 +01:00
move genres into own table (#104)
* create genres table and translate media_type * parse genres from TMDb and modify artisan db command * parse genres on migration * Save genres in new table (via relation) * Implode genres as string. Update genres in refresh. * add tests for genres * fix icon for src * display genres on own page include few ui tweaks
This commit is contained in:
parent
c3dda14427
commit
1a1753181f
@ -2,37 +2,53 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Models\GenreService;
|
||||
use App\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB as LaravelDB;
|
||||
|
||||
class DB extends Command {
|
||||
|
||||
protected $signature = 'flox:db {username?} {password?}';
|
||||
private $genreService;
|
||||
|
||||
protected $signature = 'flox:db {--fresh : Whether all data should be reset} {username?} {password?}';
|
||||
protected $description = 'Create database migrations and admin account';
|
||||
|
||||
public function __construct()
|
||||
public function __construct(GenreService $genreService)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->genreService = $genreService;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if($this->option('fresh')) {
|
||||
$this->alert('ALL DATA WILL BE REMOVED');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->createMigrations();
|
||||
} catch(\Exception $e) {
|
||||
$this->error('Can not connect to the database. Error: ' . $e->getMessage());
|
||||
$this->error('Make sure your database credentials in .env are correct');
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$this->createUser();
|
||||
}
|
||||
|
||||
private function createMigrations()
|
||||
{
|
||||
$this->info('TRYING TO MIGRATE DATABASE');
|
||||
$this->call('migrate', ['--force' => true]);
|
||||
|
||||
if($this->option('fresh')) {
|
||||
$this->call('migrate:fresh', ['--force' => true]);
|
||||
} else {
|
||||
$this->call('migrate', ['--force' => true]);
|
||||
}
|
||||
|
||||
$this->info('MIGRATION COMPLETED');
|
||||
}
|
||||
|
||||
@ -41,6 +57,10 @@
|
||||
$username = $this->ask('Enter your admin username', $this->argument("username"));
|
||||
$password = $this->ask('Enter your admin password', $this->argument("password"));
|
||||
|
||||
if($this->option('fresh')) {
|
||||
LaravelDB::table('users')->delete();
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->username = $username;
|
||||
$user->password = bcrypt($password);
|
||||
|
@ -32,7 +32,7 @@
|
||||
|
||||
public function getReleaseEpisodeHumanFormatAttribute()
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$now = now();
|
||||
$release = Carbon::createFromTimestamp($this->release_episode);
|
||||
|
||||
if($release > $now) {
|
||||
|
20
backend/app/Genre.php
Normal file
20
backend/app/Genre.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Genre extends Model {
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'id',
|
||||
];
|
||||
|
||||
public function scopeFindByName($query, $genre)
|
||||
{
|
||||
return $query->where('name', $genre);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
use App\Services\Storage;
|
||||
use App\Setting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
@ -71,30 +72,27 @@
|
||||
|
||||
$data = json_decode(file_get_contents($file));
|
||||
|
||||
$this->importItems($data, $itemService);
|
||||
$this->importSettings($data);
|
||||
$this->importEpisodes($data);
|
||||
$this->importAlternativeTitles($data);
|
||||
$this->importSettings($data);
|
||||
$this->importItems($data, $itemService);
|
||||
}
|
||||
|
||||
private function importItems($data, ItemService $itemService)
|
||||
{
|
||||
logInfo("Import Movies");
|
||||
|
||||
if(isset($data->items)) {
|
||||
$this->item->truncate();
|
||||
DB::table('items')->delete();
|
||||
|
||||
foreach($data->items as $item) {
|
||||
logInfo("Importing", [$item->title]);
|
||||
|
||||
// Fallback if export was from an older version of flox (<= 1.2.2).
|
||||
if( ! isset($item->last_seen_at)) {
|
||||
$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->downloadImages($item['poster'], $item['backdrop']);
|
||||
}
|
||||
|
||||
$this->item->create((array) $item);
|
||||
}
|
||||
|
||||
@ -107,7 +105,9 @@
|
||||
{
|
||||
logInfo("Import Tv Shows");
|
||||
if(isset($data->episodes)) {
|
||||
|
||||
$this->episodes->truncate();
|
||||
|
||||
foreach($data->episodes as $episode) {
|
||||
logInfo("Importing", [$episode->name]);
|
||||
$this->episodes->create((array) $episode);
|
||||
@ -119,7 +119,9 @@
|
||||
private function importAlternativeTitles($data)
|
||||
{
|
||||
if(isset($data->alternative_titles)) {
|
||||
|
||||
$this->alternativeTitles->truncate();
|
||||
|
||||
foreach($data->alternative_titles as $title) {
|
||||
$this->alternativeTitles->create((array) $title);
|
||||
}
|
||||
@ -129,7 +131,9 @@
|
||||
private function importSettings($data)
|
||||
{
|
||||
if(isset($data->settings)) {
|
||||
|
||||
$this->settings->truncate();
|
||||
|
||||
foreach($data->settings as $setting) {
|
||||
$this->settings->create((array) $setting);
|
||||
}
|
||||
|
28
backend/app/Http/Controllers/GenreController.php
Normal file
28
backend/app/Http/Controllers/GenreController.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Genre;
|
||||
use App\Services\Models\GenreService;
|
||||
|
||||
class GenreController {
|
||||
|
||||
private $genreService;
|
||||
private $genre;
|
||||
|
||||
public function __construct(GenreService $genreService, Genre $genre)
|
||||
{
|
||||
$this->genreService = $genreService;
|
||||
$this->genre = $genre;
|
||||
}
|
||||
|
||||
public function allGenres()
|
||||
{
|
||||
return $this->genre->all();
|
||||
}
|
||||
|
||||
public function updateGenreLists()
|
||||
{
|
||||
$this->genreService->updateGenreLists();
|
||||
}
|
||||
}
|
@ -78,11 +78,6 @@
|
||||
$alternativeTitle->update();
|
||||
}
|
||||
|
||||
public function updateGenre()
|
||||
{
|
||||
$this->itemService->updateGenre();
|
||||
}
|
||||
|
||||
public function toggleEpisode($id)
|
||||
{
|
||||
if( ! $this->episodeService->toggleSeen($id)) {
|
||||
|
@ -23,6 +23,11 @@
|
||||
{
|
||||
return $this->tmdb->suggestions($mediaType, $tmdbId);
|
||||
}
|
||||
|
||||
public function genre($genre)
|
||||
{
|
||||
return $this->tmdb->byGenre($genre);
|
||||
}
|
||||
|
||||
public function trending()
|
||||
{
|
||||
|
@ -2,12 +2,18 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Item extends Model {
|
||||
|
||||
protected $dates = ['last_seen'];
|
||||
protected $dates = [
|
||||
'last_seen_at',
|
||||
'refreshed_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $with = ['genre'];
|
||||
|
||||
protected $fillable = [
|
||||
'tmdb_id',
|
||||
@ -16,8 +22,8 @@
|
||||
'poster',
|
||||
'media_type',
|
||||
'rating',
|
||||
//'genre',
|
||||
'released',
|
||||
'genre',
|
||||
'fp_name',
|
||||
'src',
|
||||
'subtitles',
|
||||
@ -32,6 +38,7 @@
|
||||
'youtube_key',
|
||||
'slug',
|
||||
'watchlist',
|
||||
'refreshed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -50,14 +57,14 @@
|
||||
'poster' => $data['poster'] ? $data['poster'] : '',
|
||||
'rating' => 0,
|
||||
'released' => $data['released'],
|
||||
'genre' => $data['genre'],
|
||||
//'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(),
|
||||
'last_seen_at' => now(),
|
||||
'slug' => $data['slug'],
|
||||
]);
|
||||
}
|
||||
@ -79,7 +86,7 @@
|
||||
'released' => time(),
|
||||
'src' => $data['src'],
|
||||
'subtitles' => $data['subtitles'],
|
||||
'last_seen_at' => Carbon::now(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -90,13 +97,18 @@
|
||||
public function updateLastSeenAt($tmdbId)
|
||||
{
|
||||
return $this->where('tmdb_id', $tmdbId)->update([
|
||||
'last_seen_at' => Carbon::now(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Relations
|
||||
*/
|
||||
|
||||
public function genre()
|
||||
{
|
||||
return $this->belongsToMany(Genre::class);
|
||||
}
|
||||
|
||||
public function episodes()
|
||||
{
|
||||
@ -125,6 +137,13 @@
|
||||
/*
|
||||
* Scopes
|
||||
*/
|
||||
|
||||
public function scopeFindByGenreId($query, $genreId)
|
||||
{
|
||||
return $query->orWhereHas('genre', function($query) use ($genreId) {
|
||||
$query->where('genre_id', $genreId);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeFindByTmdbId($query, $tmdbId)
|
||||
{
|
||||
|
@ -7,7 +7,6 @@
|
||||
use App\Services\Models\EpisodeService;
|
||||
use App\Services\Models\ItemService;
|
||||
use App\Setting;
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@ -51,6 +50,7 @@
|
||||
public function fetch()
|
||||
{
|
||||
logInfo("Making request to flox-file-parser...");
|
||||
|
||||
$timestamp = $this->lastFetched()['last_fetch_to_file_parser'];
|
||||
$fpUrl = config('services.fp.host') . ':' . config('services.fp.port');
|
||||
$fpUri = '/fetch/' . $timestamp;
|
||||
@ -77,7 +77,7 @@
|
||||
|
||||
foreach((array) $files as $type => $items) {
|
||||
$this->itemCategory = $type;
|
||||
|
||||
|
||||
foreach($items as $item) {
|
||||
try {
|
||||
logInfo("Updating data");
|
||||
@ -100,6 +100,7 @@
|
||||
*
|
||||
* @param $item
|
||||
* @return bool|mixed|void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function handleStatus($item)
|
||||
{
|
||||
@ -241,7 +242,7 @@
|
||||
|
||||
// Otherwise create a new item from the result.
|
||||
$created = $this->itemService->create($firstResult);
|
||||
|
||||
|
||||
return $this->store($item, $created->tmdb_id);
|
||||
}
|
||||
|
||||
@ -376,7 +377,7 @@
|
||||
private function updateLastFetched()
|
||||
{
|
||||
Setting::first()->update([
|
||||
'last_fetch_to_file_parser' => Carbon::now(),
|
||||
'last_fetch_to_file_parser' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,6 @@
|
||||
* @param Model $model
|
||||
* @param TMDB $tmdb
|
||||
* @param Item $item
|
||||
* @internal param ItemService $itemService
|
||||
* @internal param Item $item
|
||||
*/
|
||||
public function __construct(Model $model, TMDB $tmdb, Item $item)
|
||||
{
|
||||
|
55
backend/app/Services/Models/GenreService.php
Normal file
55
backend/app/Services/Models/GenreService.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Models;
|
||||
|
||||
use App\Genre as Model;
|
||||
use App\Services\TMDB;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GenreService {
|
||||
|
||||
private $model;
|
||||
private $tmdb;
|
||||
|
||||
/**
|
||||
* @param Model $model
|
||||
* @param TMDB $tmdb
|
||||
*/
|
||||
public function __construct(Model $model, TMDB $tmdb)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->tmdb = $tmdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the pivot table genre_item.
|
||||
*
|
||||
* @param $item
|
||||
* @param $ids
|
||||
*/
|
||||
public function sync($item, $ids)
|
||||
{
|
||||
$item->genre()->sync($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the genres table.
|
||||
*/
|
||||
public function updateGenreLists()
|
||||
{
|
||||
$genres = $this->tmdb->getGenreLists();
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
foreach($genres as $mediaType) {
|
||||
foreach($mediaType->genres as $genre) {
|
||||
$this->model->firstOrCreate(
|
||||
['id' => $genre->id],
|
||||
['name' => $genre->name]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
use App\Services\TMDB;
|
||||
use GuzzleHttp\Client;
|
||||
use App\Setting;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ItemService {
|
||||
@ -19,6 +20,7 @@
|
||||
private $episodeService;
|
||||
private $imdb;
|
||||
private $setting;
|
||||
private $genreService;
|
||||
|
||||
/**
|
||||
* @param Model $model
|
||||
@ -26,6 +28,7 @@
|
||||
* @param Storage $storage
|
||||
* @param AlternativeTitleService $alternativeTitleService
|
||||
* @param EpisodeService $episodeService
|
||||
* @param GenreService $genreService
|
||||
* @param IMDB $imdb
|
||||
* @param Setting $setting
|
||||
*/
|
||||
@ -35,6 +38,7 @@
|
||||
Storage $storage,
|
||||
AlternativeTitleService $alternativeTitleService,
|
||||
EpisodeService $episodeService,
|
||||
GenreService $genreService,
|
||||
IMDB $imdb,
|
||||
Setting $setting
|
||||
){
|
||||
@ -45,6 +49,7 @@
|
||||
$this->episodeService = $episodeService;
|
||||
$this->imdb = $imdb;
|
||||
$this->setting = $setting;
|
||||
$this->genreService = $genreService;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,16 +58,21 @@
|
||||
*/
|
||||
public function create($data)
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
$data = $this->makeDataComplete($data);
|
||||
|
||||
|
||||
$item = $this->model->store($data);
|
||||
|
||||
$this->episodeService->create($item);
|
||||
$this->genreService->sync($item, $data['genre_ids']);
|
||||
$this->alternativeTitleService->create($item);
|
||||
|
||||
$this->storage->downloadImages($item->poster, $item->backdrop);
|
||||
|
||||
return $item;
|
||||
DB::commit();
|
||||
|
||||
return $item->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,9 +120,11 @@
|
||||
public function refreshAll()
|
||||
{
|
||||
increaseTimeLimit();
|
||||
|
||||
$this->genreService->updateGenreLists();
|
||||
|
||||
$this->model->all()->each(function($item) {
|
||||
$this->refresh($item->id);
|
||||
return $this->model->orderBy('refreshed_at')->get()->each(function($item) {
|
||||
return $this->refresh($item->id, true);
|
||||
});
|
||||
}
|
||||
|
||||
@ -160,6 +172,11 @@
|
||||
$this->episodeService->create($item);
|
||||
$this->alternativeTitleService->create($item);
|
||||
|
||||
$this->genreService->sync(
|
||||
$item,
|
||||
collect($details->genres)->pluck('id')->all()
|
||||
);
|
||||
|
||||
$this->storage->downloadImages($item->poster, $item->backdrop);
|
||||
}
|
||||
|
||||
@ -322,26 +339,7 @@
|
||||
{
|
||||
return $this->model->findByTitle($title)->with('latestEpisode')->withCount('episodesWithSrc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse full genre list of all movies and tv shows in our database and save them.
|
||||
*/
|
||||
public function updateGenre()
|
||||
{
|
||||
increaseTimeLimit();
|
||||
|
||||
$items = $this->model->all();
|
||||
|
||||
$items->each(function($item) {
|
||||
$genres = $this->tmdb->details($item->tmdb_id, $item->media_type)->genres;
|
||||
$data = collect($genres)->pluck('name')->all();
|
||||
|
||||
$item->update([
|
||||
'genre' => implode($data, ', '),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* See if we can find a item by title, fp_name, tmdb_id or src in our database.
|
||||
*
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\Models\ItemService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Subpage {
|
||||
|
||||
@ -22,6 +23,11 @@
|
||||
}
|
||||
|
||||
$found = $this->tmdb->details($tmdbId, $mediaType);
|
||||
|
||||
if( ! (array) $found) {
|
||||
return response('Not found', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$found->genre_ids = collect($found->genres)->pluck('id')->all();
|
||||
|
||||
$item = $this->tmdb->createItem($found, $mediaType);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Genre;
|
||||
use App\Item;
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client;
|
||||
@ -15,7 +16,7 @@
|
||||
private $client;
|
||||
private $apiKey;
|
||||
private $translation;
|
||||
|
||||
|
||||
private $base = 'https://api.themoviedb.org';
|
||||
|
||||
/**
|
||||
@ -33,7 +34,7 @@
|
||||
/**
|
||||
* Search TMDb by 'title'.
|
||||
*
|
||||
* @param $title
|
||||
* @param $title
|
||||
* @param null $mediaType
|
||||
* @return Collection
|
||||
*/
|
||||
@ -169,25 +170,58 @@
|
||||
return $this->filterItems($cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search TMDb by genre.
|
||||
*
|
||||
* @param $genre
|
||||
* @return array
|
||||
*/
|
||||
public function byGenre($genre)
|
||||
{
|
||||
$genreId = Genre::findByName($genre)->firstOrFail()->id;
|
||||
|
||||
$cache = Cache::remember('genre-' . $genre, $this->untilEndOfDay(), function() use ($genreId) {
|
||||
|
||||
$responseMovies = $this->requestTmdb($this->base . '/3/discover/movie', ['with_genres' => $genreId]);
|
||||
$responseTv = $this->requestTmdb($this->base . '/3/discover/tv', ['with_genres' => $genreId]);
|
||||
|
||||
$movies = collect($this->createItems($responseMovies, 'movie'));
|
||||
$tv = collect($this->createItems($responseTv, 'tv'));
|
||||
|
||||
return $tv->merge($movies)->shuffle();
|
||||
});
|
||||
|
||||
//$inDB = Item::findByGenreId($genreId)->get();
|
||||
|
||||
return $this->filterItems($cache, $genreId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the response with items from our database.
|
||||
*
|
||||
* @param Collection $items
|
||||
* @param null $genreId
|
||||
* @return array
|
||||
*/
|
||||
private function filterItems($items)
|
||||
private function filterItems($items, $genreId = null)
|
||||
{
|
||||
$allId = $items->pluck('tmdb_id');
|
||||
|
||||
// Get all movies/tv shows from trending/upcoming that are already in the database.
|
||||
$inDB = Item::whereIn('tmdb_id', $allId)->withCount('episodesWithSrc')->get()->toArray();
|
||||
// Get all movies / tv shows that are already in our database.
|
||||
$searchInDB = Item::whereIn('tmdb_id', $allId)->withCount('episodesWithSrc');
|
||||
|
||||
if($genreId) {
|
||||
$searchInDB->findByGenreId($genreId);
|
||||
}
|
||||
|
||||
// Remove all inDB movies / tv shows from trending / upcoming.
|
||||
$filtered = $items->filter(function($item) use ($inDB) {
|
||||
return ! in_array($item['tmdb_id'], array_column($inDB, 'tmdb_id'));
|
||||
$foundInDB = $searchInDB->get()->toArray();
|
||||
|
||||
// Remove them from the TMDb response.
|
||||
$filtered = $items->filter(function($item) use ($foundInDB) {
|
||||
return ! in_array($item['tmdb_id'], array_column($foundInDB, 'tmdb_id'));
|
||||
});
|
||||
|
||||
$merged = $filtered->merge($inDB);
|
||||
$merged = $filtered->merge($foundInDB);
|
||||
|
||||
// Reset array keys to display inDB items first.
|
||||
return array_values($merged->reverse()->toArray());
|
||||
@ -199,8 +233,8 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $response
|
||||
* @param $mediaType
|
||||
* @param $response
|
||||
* @param $mediaType
|
||||
* @return array
|
||||
*/
|
||||
private function createItems($response, $mediaType)
|
||||
@ -222,7 +256,7 @@
|
||||
);
|
||||
|
||||
$title = $data->name ?? $data->title;
|
||||
|
||||
|
||||
return [
|
||||
'tmdb_id' => $data->id,
|
||||
'title' => $title,
|
||||
@ -231,7 +265,8 @@
|
||||
'poster' => $data->poster_path,
|
||||
'media_type' => $mediaType,
|
||||
'released' => $release->getTimestamp(),
|
||||
'genre' => $this->parseGenre($data->genre_ids),
|
||||
'genre_ids' => $data->genre_ids,
|
||||
'genre' => Genre::whereIn('id', $data->genre_ids)->get(),
|
||||
'episodes' => [],
|
||||
'overview' => $data->overview,
|
||||
'backdrop' => $data->backdrop_path,
|
||||
@ -246,12 +281,12 @@
|
||||
'api_key' => $this->apiKey,
|
||||
'language' => strtolower($this->translation)
|
||||
], $query);
|
||||
|
||||
|
||||
try {
|
||||
$response = $this->client->get($url, [
|
||||
'query' => $query
|
||||
]);
|
||||
|
||||
|
||||
if($this->hasLimitRemaining($response)) {
|
||||
return $response;
|
||||
}
|
||||
@ -367,57 +402,16 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Create genre string from genre_ids.
|
||||
*
|
||||
* @param $ids
|
||||
* @return string
|
||||
* Get the lists of genres from TMDb for tv shows and movies.
|
||||
*/
|
||||
private function parseGenre($ids)
|
||||
{
|
||||
$genre = [];
|
||||
|
||||
foreach($ids as $id) {
|
||||
$genre[] = $this->genreList()[$id] ?? '';
|
||||
}
|
||||
|
||||
return implode($genre, ', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Current genre list from TMDb.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function genreList()
|
||||
public function getGenreLists()
|
||||
{
|
||||
$movies = $this->requestTmdb($this->base . '/3/genre/movie/list');
|
||||
$tv = $this->requestTmdb($this->base . '/3/genre/tv/list');
|
||||
|
||||
return [
|
||||
28 => 'Action',
|
||||
12 => 'Adventure',
|
||||
16 => 'Animation',
|
||||
35 => 'Comedy',
|
||||
80 => 'Crime',
|
||||
99 => 'Documentary',
|
||||
18 => 'Drama',
|
||||
10751 => 'Family',
|
||||
14 => 'Fantasy',
|
||||
36 => 'History',
|
||||
27 => 'Horror',
|
||||
10402 => 'Music',
|
||||
9648 => 'Mystery',
|
||||
10749 => 'Romance',
|
||||
878 => 'Sci-Fi',
|
||||
10770 => 'TV Movie',
|
||||
53 => 'Thriller',
|
||||
10752 => 'War',
|
||||
37 => 'Western',
|
||||
10759 => 'Action & Adventure',
|
||||
10762 => 'Kids',
|
||||
10763 => 'News',
|
||||
10764 => 'Reality',
|
||||
10765 => 'Sci-Fi & Fantasy',
|
||||
10766 => 'Soap',
|
||||
10767 => 'Talk',
|
||||
10768 => 'War & Politics',
|
||||
'movies' => json_decode($movies->getBody()),
|
||||
'tv' => json_decode($tv->getBody()),
|
||||
];
|
||||
}
|
||||
|
||||
@ -429,7 +423,7 @@
|
||||
{
|
||||
if($response->getStatusCode() == 429) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return ((int) $response->getHeader('X-RateLimit-Remaining')[0]) > 1;
|
||||
}
|
||||
@ -439,6 +433,6 @@
|
||||
*/
|
||||
private function untilEndOfDay()
|
||||
{
|
||||
return Carbon::now()->secondsUntilEndOfDay() / 60;
|
||||
return now()->secondsUntilEndOfDay() / 60;
|
||||
}
|
||||
}
|
||||
|
@ -19,4 +19,11 @@
|
||||
'show_watchlist_everywhere',
|
||||
'show_ratings',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'show_date' => 'boolean',
|
||||
'show_genre' => 'boolean',
|
||||
'episode_spoiler_protection' => 'boolean',
|
||||
'show_watchlist_everywhere' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
return [
|
||||
'poster' => '',
|
||||
'rating' => 1,
|
||||
'genre' => '',
|
||||
//'genre' => '',
|
||||
'released' => time(),
|
||||
'last_seen_at' => Carbon::now(),
|
||||
'src' => null,
|
||||
|
@ -14,10 +14,5 @@ class AddReleaseDatesToEpisodes extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('episodes', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
public function down() {}
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Services\Models\GenreService;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateGenresTable extends Migration
|
||||
{
|
||||
private $genreService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->genreService = app(GenreService::class);
|
||||
}
|
||||
|
||||
public function up()
|
||||
{
|
||||
Schema::create('genres', function (Blueprint $table) {
|
||||
$table->integer('id');
|
||||
$table->string('name');
|
||||
});
|
||||
|
||||
if( ! app()->runningUnitTests()) {
|
||||
try {
|
||||
$this->genreService->updateGenreLists();
|
||||
} catch (\Exception $e) {
|
||||
echo 'Can not connect to the TMDb Service on "CreateGenresTable". Error: ' . $e->getMessage();
|
||||
echo 'Make sure you set your TMDb API Key in .env';
|
||||
|
||||
abort(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down() {}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateGenreItemTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('genre_item', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('item_id');
|
||||
$table->integer('genre_id');
|
||||
});
|
||||
|
||||
// We dont need the genre column more.
|
||||
Schema::table('items', function (Blueprint $table) {
|
||||
$table->dropColumn('genre');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('genre_item');
|
||||
}
|
||||
}
|
@ -12,6 +12,8 @@
|
||||
Route::get('/imdb-rating/{imdbId}', 'SubpageController@imdbRating');
|
||||
|
||||
Route::get('/suggestions/{tmdbID}/{mediaType}', 'TMDBController@suggestions');
|
||||
Route::get('/genres', 'GenreController@allGenres');
|
||||
Route::get('/genre/{genre}', 'TMDBController@genre');
|
||||
Route::get('/trending', 'TMDBController@trending');
|
||||
Route::get('/upcoming', 'TMDBController@upcoming');
|
||||
Route::get('/now-playing', 'TMDBController@nowPlaying');
|
||||
@ -28,6 +30,8 @@
|
||||
Route::post('/watchlist', 'ItemController@watchlist');
|
||||
Route::patch('/update-alternative-titles/{tmdbId?}', 'ItemController@updateAlternativeTitles');
|
||||
Route::patch('/update-genre', 'ItemController@updateGenre');
|
||||
// todo: in patch
|
||||
Route::get('/update-genre-lists', 'GenreController@updateGenreLists');
|
||||
Route::patch('/toggle-episode/{id}', 'ItemController@toggleEpisode');
|
||||
Route::patch('/toggle-season', 'ItemController@toggleSeason');
|
||||
Route::patch('/change-rating/{itemId}', 'ItemController@changeRating');
|
||||
|
@ -3,7 +3,8 @@
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
trait CreatesApplication {
|
||||
|
||||
/**
|
||||
@ -16,7 +17,9 @@
|
||||
$app = require __DIR__ . '/../bootstrap/app.php';
|
||||
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
|
||||
Hash::setRounds(4);
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
|
66
backend/tests/Services/GenreServiceTest.php
Normal file
66
backend/tests/Services/GenreServiceTest.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Services;
|
||||
|
||||
use App\Genre;
|
||||
use App\Item;
|
||||
use App\Services\Models\GenreService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
use Tests\Traits\Factories;
|
||||
use Tests\Traits\Fixtures;
|
||||
use Tests\Traits\Mocks;
|
||||
|
||||
class GenreServiceTest extends TestCase {
|
||||
|
||||
use RefreshDatabase;
|
||||
use Factories;
|
||||
use Fixtures;
|
||||
use Mocks;
|
||||
|
||||
/** @test */
|
||||
public function it_should_update_the_genre_table()
|
||||
{
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/genres'),
|
||||
$this->tmdbFixtures('tv/genres')
|
||||
);
|
||||
|
||||
$service = app(GenreService::class);
|
||||
|
||||
$genresBeforeUpdate = Genre::all();
|
||||
|
||||
$service->updateGenreLists();
|
||||
|
||||
$genresAfterUpdate = Genre::all();
|
||||
|
||||
$this->assertCount(0, $genresBeforeUpdate);
|
||||
$this->assertCount(27, $genresAfterUpdate);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_sync_genres_for_an_item()
|
||||
{
|
||||
$genreIds = [28, 12, 16];
|
||||
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/genres'),
|
||||
$this->tmdbFixtures('tv/genres')
|
||||
);
|
||||
|
||||
$item = $this->createMovie();
|
||||
|
||||
$service = app(GenreService::class);
|
||||
$service->updateGenreLists();
|
||||
|
||||
$itemBeforeUpdate = Item::first();
|
||||
|
||||
$service->sync($item, $genreIds);
|
||||
|
||||
$itemAfterUpdate = Item::first();
|
||||
|
||||
$this->assertCount(0, $itemBeforeUpdate->genre);
|
||||
$this->assertCount(count($genreIds), $itemAfterUpdate->genre);
|
||||
}
|
||||
}
|
@ -162,38 +162,6 @@
|
||||
$this->assertNotNull($item1);
|
||||
$this->assertNull($item2);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_update_genre_for_a_movie()
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->createMovie();
|
||||
|
||||
$this->createGuzzleMock($this->tmdbFixtures('movie/details'));
|
||||
|
||||
$withoutGenre = Item::find(1);
|
||||
$this->actingAs($user)->patchJson('api/update-genre');
|
||||
$withGenre = Item::find(1);
|
||||
|
||||
$this->assertEmpty($withoutGenre->genre);
|
||||
$this->assertNotEmpty($withGenre->genre);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_update_genre_for_a_tv_show()
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->createTv();
|
||||
|
||||
$this->createGuzzleMock($this->tmdbFixtures('tv/details'));
|
||||
|
||||
$withoutGenre = Item::find(1);
|
||||
$this->actingAs($user)->patchJson('api/update-genre');
|
||||
$withGenre = Item::find(1);
|
||||
|
||||
$this->assertEmpty($withoutGenre->genre);
|
||||
$this->assertNotEmpty($withGenre->genre);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_parse_correct_imdb_id()
|
||||
|
@ -53,6 +53,24 @@
|
||||
$this->assertTrue($hasTv);
|
||||
$this->assertFalse($hasMovie);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_fetch_all_genres()
|
||||
{
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/genres'),
|
||||
$this->tmdbFixtures('tv/genres')
|
||||
);
|
||||
|
||||
$tmdb = app(TMDB::class);
|
||||
$genres = $tmdb->getGenreLists();
|
||||
|
||||
$this->assertArrayHasKey('tv', $genres);
|
||||
$this->assertArrayHasKey('movies', $genres);
|
||||
|
||||
$this->assertCount(16, $genres['tv']->genres);
|
||||
$this->assertCount(19, $genres['movies']->genres);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_only_search_for_movies()
|
||||
|
3
backend/tests/fixtures/flox/movie.json
vendored
3
backend/tests/fixtures/flox/movie.json
vendored
@ -6,5 +6,6 @@
|
||||
"media_type": "movie",
|
||||
"released": 1464185524,
|
||||
"genre": "Adventure, Fantasy, Action",
|
||||
"genre_ids": [1,2,3],
|
||||
"episodes": []
|
||||
}
|
||||
}
|
||||
|
3
backend/tests/fixtures/flox/tv.json
vendored
3
backend/tests/fixtures/flox/tv.json
vendored
@ -6,5 +6,6 @@
|
||||
"media_type": "tv",
|
||||
"released": 1303051450,
|
||||
"genre": "Sci-Fi & Fantasy, Action & Adventure, Drama",
|
||||
"genre_ids": [1,2,3],
|
||||
"episodes": []
|
||||
}
|
||||
}
|
||||
|
80
backend/tests/fixtures/tmdb/movie/genres.json
vendored
Normal file
80
backend/tests/fixtures/tmdb/movie/genres.json
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"genres": [
|
||||
{
|
||||
"id": 28,
|
||||
"name": "Action"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Adventure"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Animation"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "Comedy"
|
||||
},
|
||||
{
|
||||
"id": 80,
|
||||
"name": "Crime"
|
||||
},
|
||||
{
|
||||
"id": 99,
|
||||
"name": "Documentary"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Drama"
|
||||
},
|
||||
{
|
||||
"id": 10751,
|
||||
"name": "Family"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Fantasy"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "History"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "Horror"
|
||||
},
|
||||
{
|
||||
"id": 10402,
|
||||
"name": "Music"
|
||||
},
|
||||
{
|
||||
"id": 9648,
|
||||
"name": "Mystery"
|
||||
},
|
||||
{
|
||||
"id": 10749,
|
||||
"name": "Romance"
|
||||
},
|
||||
{
|
||||
"id": 878,
|
||||
"name": "Science Fiction"
|
||||
},
|
||||
{
|
||||
"id": 10770,
|
||||
"name": "TV Movie"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "Thriller"
|
||||
},
|
||||
{
|
||||
"id": 10752,
|
||||
"name": "War"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Western"
|
||||
}
|
||||
]
|
||||
}
|
68
backend/tests/fixtures/tmdb/tv/genres.json
vendored
Normal file
68
backend/tests/fixtures/tmdb/tv/genres.json
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"genres": [
|
||||
{
|
||||
"id": 10759,
|
||||
"name": "Action & Adventure"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Animation"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "Comedy"
|
||||
},
|
||||
{
|
||||
"id": 80,
|
||||
"name": "Crime"
|
||||
},
|
||||
{
|
||||
"id": 99,
|
||||
"name": "Documentary"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Drama"
|
||||
},
|
||||
{
|
||||
"id": 10751,
|
||||
"name": "Family"
|
||||
},
|
||||
{
|
||||
"id": 10762,
|
||||
"name": "Kids"
|
||||
},
|
||||
{
|
||||
"id": 9648,
|
||||
"name": "Mystery"
|
||||
},
|
||||
{
|
||||
"id": 10763,
|
||||
"name": "News"
|
||||
},
|
||||
{
|
||||
"id": 10764,
|
||||
"name": "Reality"
|
||||
},
|
||||
{
|
||||
"id": 10765,
|
||||
"name": "Sci-Fi & Fantasy"
|
||||
},
|
||||
{
|
||||
"id": 10766,
|
||||
"name": "Soap"
|
||||
},
|
||||
{
|
||||
"id": 10767,
|
||||
"name": "Talk"
|
||||
},
|
||||
{
|
||||
"id": 10768,
|
||||
"name": "War & Politics"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Western"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
{
|
||||
"presets": ["es2015", "stage-2"],
|
||||
"plugins": ["transform-runtime"],
|
||||
"plugins": [
|
||||
"transform-runtime",
|
||||
["component", [
|
||||
{
|
||||
"libraryName": "element-ui",
|
||||
"styleLibraryName": "theme-chalk"
|
||||
}
|
||||
]]
|
||||
],
|
||||
"comments": false
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,17 @@ require('../resources/sass/app.scss');
|
||||
import Vue from 'vue';
|
||||
import { mapState, mapActions, mapMutations } from 'vuex'
|
||||
|
||||
import { Checkbox } from 'element-ui';
|
||||
|
||||
Vue.use(Checkbox);
|
||||
|
||||
import SiteHeader from './components/Header.vue';
|
||||
import SiteFooter from './components/Footer.vue';
|
||||
import Login from './components/Login.vue';
|
||||
import Modal from './components/Modal/Index.vue';
|
||||
|
||||
import router from './routes';
|
||||
import store from './store/index';
|
||||
import store from './store';
|
||||
|
||||
const App = new Vue({
|
||||
store,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<main>
|
||||
<div class="content-submenu">
|
||||
<div class="content-submenu" v-if=" ! loading && items.length">
|
||||
<div class="sort-wrap no-select">
|
||||
<div class="sort-direction" @click="setUserSortDirection()">
|
||||
<i v-if="userSortDirection == 'asc'">↑</i>
|
||||
@ -139,4 +139,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
@ -22,10 +22,12 @@
|
||||
</div>
|
||||
|
||||
<div class="item-content">
|
||||
<span v-if="date == 1" class="item-year">{{ released }} <i>{{ localItem.media_type }}</i></span>
|
||||
<i class="item-has-src" v-if="hasSrc"></i>
|
||||
<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>
|
||||
<span v-if="date == 1" class="item-year">{{ released }} <i>{{ lang(localItem.media_type) }}</i></span>
|
||||
<router-link :to="{ name: `subpage-${localItem.media_type}`, params: { tmdbId: localItem.tmdb_id }}" class="item-title" :title="localItem.title">
|
||||
<i class="item-has-src" v-if="hasSrc"></i>
|
||||
{{ localItem.title }}
|
||||
</router-link>
|
||||
<span v-if="genre == 1" class="item-genre">{{ genreAsString(localItem.genre) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -1,18 +1,20 @@
|
||||
<template>
|
||||
|
||||
<div class="settings-box no-select" v-if=" ! loading">
|
||||
<div class="settings-box element-ui-checkbox no-select" v-if=" ! loading">
|
||||
|
||||
<div class="setting-box">
|
||||
<input type="checkbox" value="genre" v-model="genre" id="genre" @change="updateOptions"><label for="genre">{{ lang('display genre') }}</label>
|
||||
<el-checkbox v-model="genre" @change="updateOptions">{{ lang('display genre') }}</el-checkbox>
|
||||
</div>
|
||||
<div class="setting-box">
|
||||
<input type="checkbox" value="date" v-model="date" id="date" @change="updateOptions"><label for="date">{{ lang('display date') }}</label>
|
||||
<el-checkbox v-model="date" @change="updateOptions">{{ lang('display date') }}</el-checkbox>
|
||||
</div>
|
||||
<div class="setting-box">
|
||||
<input type="checkbox" value="spoiler" v-model="spoiler" id="spoiler" @change="updateOptions"><label for="spoiler">{{ lang('spoiler') }}</label>
|
||||
<el-checkbox v-model="spoiler" @change="updateOptions">{{ lang('spoiler') }}</el-checkbox>
|
||||
</div>
|
||||
<div class="setting-box">
|
||||
<input type="checkbox" value="watchlist" v-model="watchlist" id="watchlist" @change="updateOptions"><label for="watchlist">{{ lang('show watchlist') }}</label>
|
||||
<el-checkbox v-model="watchlist" @change="updateOptions">{{ lang('show watchlist') }}</el-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="setting-box select-box">
|
||||
<label for="ratings">{{ lang('show own ratings') }}</label>
|
||||
<select id="ratings" v-model="ratings" @change="updateOptions">
|
||||
@ -63,6 +65,7 @@
|
||||
const data = response.data;
|
||||
|
||||
this.SET_LOADING(false);
|
||||
console.log(data)
|
||||
this.genre = data.genre;
|
||||
this.date = data.date;
|
||||
this.spoiler = data.spoiler;
|
||||
|
@ -18,9 +18,11 @@
|
||||
|
||||
<!-- todo: move to own component -->
|
||||
<div class="big-teaser-item-data">
|
||||
<span class="item-year">{{ released }}, <i>{{ item.media_type }}</i></span>
|
||||
<span class="item-year">{{ released }}, <i>{{ lang(item.media_type) }}</i></span>
|
||||
<span class="item-title">{{ item.title }}</span>
|
||||
<span class="item-genre">{{ item.genre }}</span>
|
||||
<span class="item-genre">
|
||||
<router-link :key="genre.id" :to="'/genre/' + genre.name" v-for="genre in item.genre">{{ genre.name }}</router-link>
|
||||
</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> {{ lang('watch trailer') }}</span>
|
||||
@ -207,8 +209,9 @@
|
||||
this.disableLoading();
|
||||
this.fetchImdbRating();
|
||||
}, error => {
|
||||
alert(error);
|
||||
console.log(error);
|
||||
this.SET_LOADING(false);
|
||||
this.$router.push('/');
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1,8 +1,23 @@
|
||||
<template>
|
||||
<main :class="{'display-suggestions': path === 'suggestions'}">
|
||||
<div class="content-submenu" v-if=" ! loading && items.length">
|
||||
<div class="sort-wrap no-select" v-if="isGenrePage">
|
||||
<div class="filter-wrap">
|
||||
<span class="current-filter" @click="toggleShowGenres()">{{ currentGenre }} <span class="arrow-down"></span></span>
|
||||
<ul class="all-filters" :class="{active: showFilters}">
|
||||
<router-link :to="'/genre/' + genre.name" v-if="genre.name !== currentGenre" v-for="genre in genres" :key="genre.id">{{ genre.name }}</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="show-watchlist-items element-ui-checkbox" @click="toggleWatchlistItems()">
|
||||
<el-checkbox v-model="showWatchlistItems">Watchlist</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap-content" v-if=" ! loading">
|
||||
<div class="items-wrap">
|
||||
<Item v-for="(item, index) in items"
|
||||
v-if=" ! item.watchlist || (showWatchlistItems && item.watchlist)"
|
||||
:item="item"
|
||||
:key="index"
|
||||
:genre="true"
|
||||
@ -35,6 +50,10 @@
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
genres: [],
|
||||
isGenrePage: false,
|
||||
currentGenre: '',
|
||||
showWatchlistItems: false,
|
||||
path: '',
|
||||
displayRatings: null
|
||||
}
|
||||
@ -42,12 +61,13 @@
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
loading: state => state.loading
|
||||
loading: state => state.loading,
|
||||
showFilters: state => state.showFilters
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations([ 'SET_LOADING' ]),
|
||||
...mapMutations([ 'SET_LOADING', 'SET_SHOW_FILTERS' ]),
|
||||
...mapActions([ 'setPageTitle' ]),
|
||||
|
||||
init() {
|
||||
@ -57,6 +77,9 @@
|
||||
switch(this.path) {
|
||||
case 'suggestions':
|
||||
return this.initSuggestions();
|
||||
case 'genre':
|
||||
this.isGenrePage = true;
|
||||
return this.initContentByGenre();
|
||||
case 'trending':
|
||||
case 'upcoming':
|
||||
case 'now-playing':
|
||||
@ -64,6 +87,38 @@
|
||||
}
|
||||
},
|
||||
|
||||
toggleWatchlistItems() {
|
||||
this.showWatchlistItems = ! this.showWatchlistItems;
|
||||
},
|
||||
|
||||
toggleShowGenres() {
|
||||
this.SET_SHOW_FILTERS( ! this.showFilters);
|
||||
},
|
||||
|
||||
initAllGenres() {
|
||||
http(`${config.api}/genres`).then(response => {
|
||||
this.genres = response.data;
|
||||
}, error => {
|
||||
console.log(error);
|
||||
})
|
||||
},
|
||||
|
||||
initContentByGenre() {
|
||||
this.initAllGenres();
|
||||
|
||||
this.currentGenre = this.$route.params.genre;
|
||||
|
||||
this.setPageTitle(this.lang('genre'));
|
||||
|
||||
http(`${config.api}/genre/${this.currentGenre}`).then(response => {
|
||||
this.items = response.data;
|
||||
this.SET_LOADING(false);
|
||||
}, error => {
|
||||
console.log(error);
|
||||
this.$router.push('/')
|
||||
})
|
||||
},
|
||||
|
||||
initSuggestions() {
|
||||
const tmdbID = this.$route.query.for;
|
||||
const type = this.$route.query.type;
|
||||
|
@ -27,7 +27,7 @@
|
||||
<script>
|
||||
import Search from './Search.vue';
|
||||
import MiscHelper from '../helpers/misc';
|
||||
import store from '../store/index';
|
||||
import store from '../store';
|
||||
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
|
||||
|
@ -15,6 +15,14 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
genreAsString(genre) {
|
||||
if(typeof genre == 'object') {
|
||||
return genre.map(item => item.name).join(', ');
|
||||
}
|
||||
|
||||
return genre
|
||||
},
|
||||
|
||||
displaySeason(item) {
|
||||
return item.media_type == 'tv' && item.rating != null && item.tmdb_id && ! item.watchlist;
|
||||
@ -68,4 +76,4 @@ export default {
|
||||
return '01';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export default new Router({
|
||||
{ path: '/movies', component: Content, name: 'movie' },
|
||||
{ path: '/tv', component: Content, name: 'tv' },
|
||||
{ path: '/watchlist/:type?', component: Content, name: 'watchlist' },
|
||||
|
||||
|
||||
{ path: '/movies/:tmdbId/:slug?', component: Subpage, name: 'subpage-movie', props: {mediaType: 'movie'} },
|
||||
{ path: '/tv/:tmdbId/:slug?', component: Subpage, name: 'subpage-tv', props: {mediaType: 'tv'} },
|
||||
|
||||
@ -31,6 +31,7 @@ export default new Router({
|
||||
{ path: '/trending', component: TMDBContent, name: 'trending' },
|
||||
{ path: '/upcoming', component: TMDBContent, name: 'upcoming' },
|
||||
{ path: '/now-playing', component: TMDBContent, name: 'now-playing' },
|
||||
{ path: '/genre/:genre', component: TMDBContent, name: 'genre' },
|
||||
|
||||
{ path: '*', redirect: '/' }
|
||||
]
|
||||
|
8688
client/package-lock.json
generated
Normal file
8688
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,9 @@
|
||||
"axios": "^0.17.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"debounce": "^1.1.0",
|
||||
"element-ui": "^2.0.10",
|
||||
"vue": "^2.5.2",
|
||||
"vue-checkbox-radio": "^0.6.0",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.0.0"
|
||||
},
|
||||
@ -16,6 +18,7 @@
|
||||
"autoprefixer": "^7.1.6",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-component": "^1.0.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
|
@ -9,7 +9,7 @@
|
||||
<title>Flox</title>
|
||||
<link rel="stylesheet" href="{{ url('assets/app.css') }}">
|
||||
<link href="{{ url('assets/favicon.ico?v=3') }}" rel="icon" type="image/x-icon">
|
||||
|
||||
|
||||
</head>
|
||||
<body
|
||||
data-url="{{ url('/') }}"
|
||||
@ -32,7 +32,7 @@
|
||||
<site-footer></site-footer>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
<script src="{{ url('assets/vendor.js') }}"></script>
|
||||
<script src="{{ url('assets/app.js') }}"></script>
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "تلفاز",
|
||||
"movies": "أفلام",
|
||||
"movie": "Movie",
|
||||
"last seen": "آخر المشاهدة",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "TV",
|
||||
"movies": "Movies",
|
||||
"movie": "Movie",
|
||||
"last seen": "Sidst set",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Aktuell",
|
||||
"tv": "TV",
|
||||
"movies": "Filme",
|
||||
"movie": "Film",
|
||||
"last seen": "Zuletzt gesehen",
|
||||
"own rating": "Eigene Bewertung",
|
||||
"title": "Titel",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "TV",
|
||||
"movies": "Ταινίες",
|
||||
"movie": "Movie",
|
||||
"last seen": "Τελευταία προβολή",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "TV",
|
||||
"movies": "Movies",
|
||||
"movie": "Movie",
|
||||
"last seen": "Last seen",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
@ -19,6 +19,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "TV",
|
||||
"movies": "Películas",
|
||||
"movie": "Movie",
|
||||
"last seen": "Última vista",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "Séries TV",
|
||||
"movies": "Films",
|
||||
"movie": "Movie",
|
||||
"last seen": "Dernier film/épisode vu",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "TV",
|
||||
"movies": "Films",
|
||||
"movie": "Movie",
|
||||
"last seen": "Laatst gezien",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"now playing": "Now Playing",
|
||||
"tv": "Сериалы",
|
||||
"movies": "Фильмы",
|
||||
"movie": "Movie",
|
||||
"last seen": "Последние просмотренные",
|
||||
"own rating": "Own Rating",
|
||||
"title": "Title",
|
||||
|
29
client/resources/sass/_element-ui.scss
vendored
Normal file
29
client/resources/sass/_element-ui.scss
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
@import '../../node_modules/element-ui/lib/theme-chalk/checkbox.css';
|
||||
|
||||
.element-ui-checkbox {
|
||||
.el-checkbox__inner {
|
||||
background: transparent;
|
||||
border-radius: 0 !important;
|
||||
transition: none;
|
||||
border: 1px solid darken(#626262, 10%) !important;
|
||||
|
||||
&:after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox__label {
|
||||
color: #888 !important;
|
||||
padding-left: 7px;
|
||||
|
||||
.dark & {
|
||||
color: #626262 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox__input.is-checked .el-checkbox__inner,
|
||||
.el-checkbox__input.is-indeterminate .el-checkbox__inner {
|
||||
border: 1px solid transparent !important;
|
||||
background: $main2;
|
||||
}
|
||||
}
|
1
client/resources/sass/_misc.scss
vendored
1
client/resources/sass/_misc.scss
vendored
@ -4,6 +4,7 @@ $dark: #484848;
|
||||
$rating1: #6bb01a;
|
||||
$rating2: #da9527;
|
||||
$rating3: #cd2727;
|
||||
$gradient: linear-gradient(to right, $main1, $main2);
|
||||
|
||||
@mixin transition($type, $type2: '', $type3: '') {
|
||||
@if $type3 != '' {
|
||||
|
4
client/resources/sass/app.scss
vendored
4
client/resources/sass/app.scss
vendored
@ -6,10 +6,12 @@
|
||||
'shake',
|
||||
'base',
|
||||
|
||||
'element-ui',
|
||||
|
||||
'components/header',
|
||||
'components/search',
|
||||
'components/content',
|
||||
'components/subpage',
|
||||
'components/login',
|
||||
'components/footer',
|
||||
'components/modal';
|
||||
'components/modal';
|
||||
|
27
client/resources/sass/components/_content.scss
vendored
27
client/resources/sass/components/_content.scss
vendored
@ -139,6 +139,10 @@ main {
|
||||
//border: 1px solid $main2;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 2px 2px $main1;
|
||||
}
|
||||
}
|
||||
|
||||
.item-image {
|
||||
@ -224,7 +228,8 @@ main {
|
||||
|
||||
.item-has-src {
|
||||
float: left;
|
||||
margin: 5px 0 0 0;
|
||||
//margin: 5px 0 0 0;
|
||||
margin: 7px 6px 0 0;
|
||||
font-style: normal;
|
||||
width: 12px;
|
||||
height: 9px;
|
||||
@ -444,7 +449,7 @@ main {
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
padding: 3px 6px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
@ -493,7 +498,8 @@ main {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.fade-enter {
|
||||
.fade-enter,
|
||||
.fade-leave {
|
||||
top: -10px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
@ -582,7 +588,8 @@ main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.current-filter {
|
||||
.current-filter,
|
||||
.show-watchlist-items {
|
||||
float: left;
|
||||
padding: 7px 9px;
|
||||
color: #888;
|
||||
@ -593,6 +600,10 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
.show-watchlist-items {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
@ -614,6 +625,8 @@ main {
|
||||
//display: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
@include transition(opacity, top);
|
||||
|
||||
@ -624,12 +637,14 @@ main {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
li {
|
||||
li,
|
||||
a {
|
||||
float: left;
|
||||
padding: 5px 10px;
|
||||
color: rgba(#fff, .9);
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid rgba(#fff, .3);
|
||||
|
||||
@include transition(background);
|
||||
@ -873,7 +888,7 @@ main {
|
||||
|
||||
.setting-btn {
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
border: 0;
|
||||
|
@ -3,7 +3,7 @@ footer {
|
||||
width: 100%;
|
||||
float: left;
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
|
||||
.open-modal & {
|
||||
padding: 40px 16px 0 0;
|
||||
@ -91,4 +91,4 @@ footer {
|
||||
&:active {
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
header {
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
width: 100%;
|
||||
padding: 25px 0;
|
||||
position: relative;
|
||||
|
6
client/resources/sass/components/_login.scss
vendored
6
client/resources/sass/components/_login.scss
vendored
@ -11,7 +11,7 @@
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
}
|
||||
|
||||
.logo-login {
|
||||
@ -38,7 +38,7 @@
|
||||
|
||||
input[type="submit"] {
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
border: 0;
|
||||
@ -64,4 +64,4 @@
|
||||
color: $rating3;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
client/resources/sass/components/_modal.scss
vendored
4
client/resources/sass/components/_modal.scss
vendored
@ -43,7 +43,7 @@
|
||||
|
||||
.modal-header {
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
color: #fff;
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
@ -261,4 +261,4 @@
|
||||
&.seen i {
|
||||
background: url(../../../public/assets/img/seen-active.png) no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
client/resources/sass/components/_subpage.scss
vendored
28
client/resources/sass/components/_subpage.scss
vendored
@ -24,7 +24,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: $main2;
|
||||
background: linear-gradient(to right, $main1, $main2);
|
||||
background: $gradient;
|
||||
width: 100%;
|
||||
//height: 65vh;
|
||||
//height: 600px;
|
||||
@ -206,13 +206,36 @@
|
||||
.item-genre {
|
||||
color: rgba(#fff, .8);
|
||||
|
||||
a {
|
||||
color: rgba(#fff, .8);
|
||||
text-decoration: none;
|
||||
background: rgba($dark, .5);
|
||||
font-size: 14px;
|
||||
padding: 3px 7px;
|
||||
margin: 0 5px 0 0;
|
||||
|
||||
@include transition(background);
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $dark;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba($dark, .7);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-style: normal;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@include media(4) {
|
||||
font-size: 14px;
|
||||
//font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,6 +283,7 @@
|
||||
.subpage-content {
|
||||
width: 100%;
|
||||
float: left;
|
||||
padding: 0 0 50px 0;
|
||||
//padding: calc(65vh - 90px) 0 0 0;
|
||||
//padding: 520px 0 0 0;
|
||||
|
||||
|
@ -31,7 +31,7 @@ module.exports = {
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|svg)$/,
|
||||
test: /\.(png|jpg|svg|woff|woff2|eot|ttf)$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
@ -42,7 +42,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
test: /\.(scss|css)$/,
|
||||
use: ExtractTextPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: ['css-loader', 'postcss-loader', 'sass-loader']
|
||||
|
Loading…
Reference in New Issue
Block a user