mirror of
https://github.com/devfake/flox.git
synced 2024-11-14 22:22:39 +01:00
commit
0c260b5167
21
README.md
21
README.md
@ -1,5 +1,7 @@
|
||||
Flox
|
||||
===============
|
||||
[![Build Status](https://travis-ci.org/devfake/flox.svg?branch=master)](https://travis-ci.org/devfake/flox)
|
||||
|
||||
Flox is a self hosted Movie, Series and Animes watch list. It's build on top of Laravel and Vue.js and uses [The Movie Database](https://www.themoviedb.org/) API.
|
||||
The rating based on an 3-Point system for `good`, `medium` and `bad`.
|
||||
|
||||
@ -33,17 +35,19 @@ php artisan flox:db # Running migrations and enter your admin credentials for th
|
||||
* Set your `CLIENT_URI` in `backend/.env`.
|
||||
```bash
|
||||
# CLIENT_URI=/flox/public
|
||||
http://localhost:8888/flox/public
|
||||
https://localhost:8888/flox/public
|
||||
|
||||
# CLIENT_URI=/subfolder/for/flox/public
|
||||
http://mydomain.com/subfolder/for/flox/public
|
||||
https://mydomain.com/subfolder/for/flox/public
|
||||
|
||||
# CLIENT_URI=/
|
||||
http://mydomain.com
|
||||
https://mydomain.com
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- API for Plex.
|
||||
- Sync movies, shows and watched episodes from Plex to Flox.
|
||||
- Episode Tracking.
|
||||
- Suggestions.
|
||||
- Watchlist.
|
||||
@ -52,6 +56,17 @@ http://mydomain.com
|
||||
- Calendar.
|
||||
- A simple calendar for your episodes and movies.
|
||||
- Movies and tv shows have different colors for better differentiation. You can also use the arrow keys to jump months forward or backward.
|
||||
- Reminders.
|
||||
|
||||
### Plex
|
||||
|
||||
To enable the sync from Plex to Flox, you first need to generate an API-Key in Flox in the settings page. Then enter the Flox API-URL to the webhooks section in Plex.
|
||||
|
||||
```
|
||||
https://YOUR-FLOX-URL/api/plex?token=YOUR-TOKEN
|
||||
```
|
||||
|
||||
If you start a tv show or movie in Plex, Flox will search the item via the title from TMDb and add them into the Flox database. If you rate a movie or tv show in Plex, Flox will also rate the item. Note that rating for seasons or episodes are not supported in Flox. If you rate an movie or tv show, which is not in the Flox database, Flox will also fetch them from TMDb first. If you complete an episode (passing the 90% mark), Flox will also check this episode as seen.
|
||||
|
||||
### Update Process
|
||||
|
||||
|
@ -73,6 +73,22 @@
|
||||
return $query->where('tmdb_id', $tmdbId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to find the result via episode_number.
|
||||
*/
|
||||
public function scopeFindByEpisodeNumber($query, $number)
|
||||
{
|
||||
return $query->where('episode_number', $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to find the result via season_number.
|
||||
*/
|
||||
public function scopeFindBySeasonNumber($query, $number)
|
||||
{
|
||||
return $query->where('season_number', $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to find the result via src.
|
||||
*/
|
||||
|
25
backend/app/Http/Controllers/ApiController.php
Normal file
25
backend/app/Http/Controllers/ApiController.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Api\Plex;
|
||||
|
||||
class ApiController {
|
||||
|
||||
/**
|
||||
* @var Plex
|
||||
*/
|
||||
private $plex;
|
||||
|
||||
public function __construct(Plex $plex)
|
||||
{
|
||||
$this->plex = $plex;
|
||||
}
|
||||
|
||||
public function plex()
|
||||
{
|
||||
$payload = json_decode(request('payload'), true);
|
||||
|
||||
$this->plex->handle($payload);
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SettingController {
|
||||
@ -68,6 +69,32 @@
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function generateApiKey()
|
||||
{
|
||||
if (isDemo()) {
|
||||
return response('Success', Response::HTTP_OK);
|
||||
}
|
||||
|
||||
$key = Str::random(24);
|
||||
|
||||
Auth::user()->update([
|
||||
'api_key' => $key,
|
||||
]);
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getApiKey()
|
||||
{
|
||||
return Auth::user()->api_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Http\Middleware\VerifyApiKey;
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
@ -53,5 +54,6 @@ class Kernel extends HttpKernel
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
|
||||
'api_key' => VerifyApiKey::class,
|
||||
];
|
||||
}
|
||||
|
41
backend/app/Http/Middleware/VerifyApiKey.php
Normal file
41
backend/app/Http/Middleware/VerifyApiKey.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\User;
|
||||
use Closure;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class VerifyApiKey
|
||||
{
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (!$request->token) {
|
||||
return response(['message' => 'No token provided'], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (!$this->user->findByApiKey($request->token)->exists()) {
|
||||
return response(['message' => 'No valid token provided'], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -55,14 +55,16 @@
|
||||
*/
|
||||
public function store($data)
|
||||
{
|
||||
return $this->create([
|
||||
return $this->firstOrCreate([
|
||||
'tmdb_id' => $data['tmdb_id'],
|
||||
], [
|
||||
'title' => $data['title'],
|
||||
'media_type' => $data['media_type'],
|
||||
'original_title' => $data['original_title'],
|
||||
'poster' => $data['poster'] ?? '',
|
||||
'rating' => 0,
|
||||
'released' => $data['released'],
|
||||
'released_timestamp' => Carbon::parse($data['released']),
|
||||
'overview' => $data['overview'],
|
||||
'backdrop' => $data['backdrop'],
|
||||
'tmdb_rating' => $data['tmdb_rating'],
|
||||
@ -92,6 +94,7 @@
|
||||
'poster' => '',
|
||||
'rating' => 0,
|
||||
'released' => time(),
|
||||
'released_timestamp' => now(),
|
||||
'src' => $data['src'],
|
||||
'subtitles' => $data['subtitles'],
|
||||
'last_seen_at' => now(),
|
||||
@ -181,6 +184,14 @@
|
||||
return $query->where('tmdb_id', $tmdbId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to find the result by year.
|
||||
*/
|
||||
public function scopeFindByYear($query, $year)
|
||||
{
|
||||
return $query->whereYear('released_timestamp', $year);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to find the result via fp_name.
|
||||
*/
|
||||
|
160
backend/app/Services/Api/Api.php
Normal file
160
backend/app/Services/Api/Api.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Api;
|
||||
|
||||
use App\Episode;
|
||||
use App\Item;
|
||||
use App\Services\Models\ItemService;
|
||||
use App\Services\TMDB;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class Api
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* @var Item
|
||||
*/
|
||||
private $item;
|
||||
|
||||
/**
|
||||
* @var TMDB
|
||||
*/
|
||||
private $tmdb;
|
||||
|
||||
/**
|
||||
* @var ItemService
|
||||
*/
|
||||
private $itemService;
|
||||
|
||||
/**
|
||||
* @var Episode
|
||||
*/
|
||||
private $episode;
|
||||
|
||||
public function __construct(Item $item, TMDB $tmdb, ItemService $itemService, Episode $episode)
|
||||
{
|
||||
$this->item = $item;
|
||||
$this->tmdb = $tmdb;
|
||||
$this->itemService = $itemService;
|
||||
$this->episode = $episode;
|
||||
}
|
||||
|
||||
public function handle(array $data)
|
||||
{
|
||||
logInfo('api data:', $data);
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
if ($this->abortRequest()) {
|
||||
abort(Response::HTTP_NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
$found = $this->item
|
||||
->findByTitle($this->getTitle(), $this->getType())
|
||||
->first();
|
||||
|
||||
// Nothing found in our database, so we search in TMDb.
|
||||
if (!$found) {
|
||||
$foundFromTmdb = $this->tmdb->search($this->getTitle(), $this->getType());
|
||||
|
||||
if (!$foundFromTmdb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The first result is mostly the one we need.
|
||||
$firstResult = $foundFromTmdb[0];
|
||||
|
||||
// Search again in our database with the TMDb ID.
|
||||
$found = $this->item->findByTmdbId($firstResult['tmdb_id'])->first();
|
||||
|
||||
if (!$found) {
|
||||
$found = $this->itemService->create($firstResult);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->shouldRateItem()) {
|
||||
$found->update([
|
||||
'rating' => $this->getRating(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->shouldEpisodeMarkedAsSeen()) {
|
||||
$episode = $this->episode
|
||||
->findByTmdbId($found->tmdb_id)
|
||||
->findByEpisodeNumber($this->getEpisodeNumber())
|
||||
->findBySeasonNumber($this->getSeasonNumber())
|
||||
->first();
|
||||
|
||||
if ($episode) {
|
||||
$episode->update([
|
||||
'seen' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the complete request if it's not a movie or episode.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function abortRequest();
|
||||
|
||||
/**
|
||||
* Is it a movie or tv show? Should return 'tv' or 'movie'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getType();
|
||||
|
||||
/**
|
||||
* Title for the item (name of the movie or tv show).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getTitle();
|
||||
|
||||
/**
|
||||
* Rating for flox in a 3-Point system.
|
||||
*
|
||||
* 1 = Good.
|
||||
* 2 = Medium.
|
||||
* 3 = Bad.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract protected function getRating();
|
||||
|
||||
/**
|
||||
* Check if rating is requested.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function shouldRateItem();
|
||||
|
||||
/**
|
||||
* Check if seen episode is requested.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function shouldEpisodeMarkedAsSeen();
|
||||
|
||||
/**
|
||||
* Number of the episode.
|
||||
*
|
||||
* @return null|int
|
||||
*/
|
||||
abstract protected function getEpisodeNumber();
|
||||
|
||||
/**
|
||||
* Number of the season.
|
||||
*
|
||||
* @return null|int
|
||||
*/
|
||||
abstract protected function getSeasonNumber();
|
||||
}
|
86
backend/app/Services/Api/Plex.php
Normal file
86
backend/app/Services/Api/Plex.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Api;
|
||||
|
||||
class Plex extends Api
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function abortRequest()
|
||||
{
|
||||
return !in_array($this->data['Metadata']['type'], ['episode', 'show', 'movie']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getType()
|
||||
{
|
||||
$type = $this->data['Metadata']['type'];
|
||||
|
||||
return in_array($type, ['episode', 'show']) ? 'tv' : 'movie';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getTitle()
|
||||
{
|
||||
return $this->data['Metadata']['grandparentTitle'] ?? $this->data['Metadata']['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function shouldRateItem()
|
||||
{
|
||||
$type = $this->data['Metadata']['type'];
|
||||
|
||||
// Flox has no ratings for seasons or episodes.
|
||||
return in_array($type, ['show', 'movie']) && $this->data['event'] === 'media.rate';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getRating()
|
||||
{
|
||||
$rating = $this->data['Metadata']['userRating'];
|
||||
|
||||
if ($rating > 7) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($rating > 4) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function shouldEpisodeMarkedAsSeen()
|
||||
{
|
||||
return $this->data['event'] === 'media.scrobble' && $this->getType() === 'tv';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getEpisodeNumber()
|
||||
{
|
||||
return $this->data['Metadata']['index'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getSeasonNumber()
|
||||
{
|
||||
return $this->data['Metadata']['parentIndex'] ?? null;
|
||||
}
|
||||
}
|
@ -83,11 +83,12 @@
|
||||
{
|
||||
Carbon::setLocale(config('app.TRANSLATION'));
|
||||
|
||||
$episodes = $this->model->findByTmdbId($tmdbId)->get();
|
||||
$episodes = $this->model->findByTmdbId($tmdbId)->oldest('episode_number')->get()->groupBy('season_number');
|
||||
$nextEpisode = $this->model->findByTmdbId($tmdbId)->where('seen', 0)->oldest('season_number')->oldest('episode_number')->first();
|
||||
|
||||
return [
|
||||
'episodes' => $episodes->sortBy('episode_number')->groupBy('season_number'),
|
||||
'next_episode' => $episodes->where('seen', 0)->first(),
|
||||
'episodes' => $episodes,
|
||||
'next_episode' => $nextEpisode,
|
||||
'spoiler' => Setting::first()->episode_spoiler_protection,
|
||||
];
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
*
|
||||
* @param $title
|
||||
* @param null $mediaType
|
||||
* @return Collection
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|Collection
|
||||
*/
|
||||
public function search($title, $mediaType = null)
|
||||
{
|
||||
@ -52,7 +52,7 @@
|
||||
$tv = collect($this->createItems($response, 'tv'));
|
||||
}
|
||||
|
||||
if( ! $mediaType || $mediaType == 'movies') {
|
||||
if( ! $mediaType || $mediaType == 'movies' || $mediaType == 'movie') {
|
||||
$response = $this->fetchSearch($title, 'movie');
|
||||
$movies = collect($this->createItems($response, 'movie'));
|
||||
}
|
||||
@ -272,7 +272,8 @@
|
||||
'original_title' => $data->original_name ?? $data->original_title,
|
||||
'poster' => $data->poster_path,
|
||||
'media_type' => $mediaType,
|
||||
'released' => $release->getTimestamp(),
|
||||
'released' => $release->copy()->getTimestamp(),
|
||||
'released_timestamp' => $release,
|
||||
'genre_ids' => $data->genre_ids,
|
||||
'genre' => Genre::whereIn('id', $data->genre_ids)->get(),
|
||||
'episodes' => [],
|
||||
|
@ -14,6 +14,7 @@
|
||||
protected $fillable = [
|
||||
'username',
|
||||
'password',
|
||||
'api_key',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -25,4 +26,12 @@
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope to find a user by an api key.
|
||||
*/
|
||||
public function scopeFindByApiKey($query, $key)
|
||||
{
|
||||
return $query->where('api_key', $key);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
return [
|
||||
|
||||
// Current version. Is synced with git releases.
|
||||
'version' => '2.0.0',
|
||||
'version' => '2.1.0',
|
||||
|
||||
'TRANSLATION' => env('TRANSLATION', 'EN'),
|
||||
'LOADING_ITEMS' => env('LOADING_ITEMS'),
|
||||
|
@ -9,6 +9,7 @@
|
||||
'username' => $faker->name,
|
||||
'password' => $password ?: $password = bcrypt('secret'),
|
||||
'remember_token' => Illuminate\Support\Str::random(10),
|
||||
'api_key' => null,
|
||||
];
|
||||
});
|
||||
|
||||
@ -27,7 +28,8 @@
|
||||
'rating' => 1,
|
||||
//'genre' => '',
|
||||
'released' => time(),
|
||||
'last_seen_at' => Carbon::now(),
|
||||
'released_timestamp' => now(),
|
||||
'last_seen_at' => now(),
|
||||
'src' => null,
|
||||
];
|
||||
});
|
||||
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddApiKeyToUsers extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('api_key')->nullable()->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(){}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Item;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddReleasedTimestampToItems extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('items', function (Blueprint $table) {
|
||||
$table->timestamp('released_timestamp')->nullable();
|
||||
});
|
||||
|
||||
Item::query()->each(function (Item $item) {
|
||||
$item->update([
|
||||
'released_timestamp' => Carbon::parse($item->released),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(){}
|
||||
}
|
@ -23,10 +23,16 @@
|
||||
Route::patch('/refresh-all', 'ItemController@refreshAll');
|
||||
Route::get('/settings', 'SettingController@settings');
|
||||
|
||||
Route::middleware('api_key')->group(function() {
|
||||
Route::post('plex', 'ApiController@plex');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->group(function() {
|
||||
Route::get('/check-update', 'SettingController@checkForUpdate');
|
||||
Route::get('/version', 'SettingController@getVersion');
|
||||
Route::get('/api-key', 'SettingController@getApiKey');
|
||||
Route::patch('/settings/refresh', 'SettingController@updateRefresh');
|
||||
Route::patch('/settings/api-key', 'SettingController@generateApiKey');
|
||||
Route::patch('/settings/reminders-send-to', 'SettingController@updateRemindersSendTo');
|
||||
Route::patch('/settings/reminder-options', 'SettingController@updateReminderOptions');
|
||||
Route::patch('/settings', 'SettingController@updateSettings');
|
||||
|
191
backend/tests/Services/Api/ApiTest.php
Normal file
191
backend/tests/Services/Api/ApiTest.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Services\Api;
|
||||
|
||||
use App\Episode;
|
||||
use App\Item;
|
||||
use App\Services\Api\Plex;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
use Tests\Traits\Factories;
|
||||
use Tests\Traits\Fixtures;
|
||||
use Tests\Traits\Mocks;
|
||||
|
||||
class ApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use Mocks;
|
||||
use Factories;
|
||||
use Fixtures;
|
||||
|
||||
public $apiClass;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createStorageDownloadsMock();
|
||||
$this->createImdbRatingMock();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function token_needs_to_be_provided()
|
||||
{
|
||||
$response = $this->postJson('api/plex');
|
||||
|
||||
$response->assertJson(['message' => 'No token provided']);
|
||||
$response->assertStatus(Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function valid_token_needs_to_be_provided()
|
||||
{
|
||||
$mock = $this->mock(Plex::class);
|
||||
$mock->shouldReceive('handle')->once()->andReturn(null);
|
||||
$user = $this->createUser(['api_key' => Str::random(24)]);
|
||||
|
||||
$responseBefore = $this->postJson('api/plex', ['token' => 'not-valid']);
|
||||
$responseAfter = $this->postJson('api/plex', ['token' => $user->api_key, 'payload' => '[]']);
|
||||
|
||||
$responseBefore->assertJson(['message' => 'No valid token provided']);
|
||||
$responseBefore->assertStatus(Response::HTTP_UNAUTHORIZED);
|
||||
|
||||
$responseAfter->assertSuccessful();
|
||||
}
|
||||
|
||||
public function it_should_abort_the_request($fixture)
|
||||
{
|
||||
$api = app($this->apiClass);
|
||||
|
||||
try {
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
} catch (HttpException $exception) {
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function it_should_create_a_new_movie($fixture)
|
||||
{
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('movie/movie'),
|
||||
$this->tmdbFixtures('movie/details'),
|
||||
$this->tmdbFixtures('movie/alternative_titles')
|
||||
);
|
||||
|
||||
$api = app($this->apiClass);
|
||||
|
||||
$itemsBefore = Item::all();
|
||||
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
|
||||
$itemsAfter = Item::all();
|
||||
|
||||
$this->assertCount(0, $itemsBefore);
|
||||
$this->assertCount(1, $itemsAfter);
|
||||
}
|
||||
|
||||
public function it_should_not_create_a_new_movie_if_it_exists($fixture)
|
||||
{
|
||||
$this->createMovie();
|
||||
|
||||
$api = app($this->apiClass);
|
||||
|
||||
$itemsBefore = Item::all();
|
||||
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
|
||||
$itemsAfter = Item::all();
|
||||
|
||||
$this->assertCount(1, $itemsBefore);
|
||||
$this->assertCount(1, $itemsAfter);
|
||||
}
|
||||
|
||||
public function it_should_create_a_new_tv_show($fixture)
|
||||
{
|
||||
$this->createGuzzleMock(
|
||||
$this->tmdbFixtures('tv/tv'),
|
||||
$this->tmdbFixtures('tv/details'),
|
||||
$this->tmdbFixtures('tv/alternative_titles')
|
||||
);
|
||||
|
||||
$this->createTmdbEpisodeMock();
|
||||
|
||||
$api = app($this->apiClass);
|
||||
|
||||
$itemsBefore = Item::all();
|
||||
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
|
||||
$itemsAfter = Item::all();
|
||||
|
||||
$this->assertCount(0, $itemsBefore);
|
||||
$this->assertCount(1, $itemsAfter);
|
||||
}
|
||||
|
||||
public function it_should_not_create_a_new_tv_show_if_it_exists($fixture)
|
||||
{
|
||||
$this->createTv();
|
||||
|
||||
$api = app($this->apiClass);
|
||||
|
||||
$itemsBefore = Item::all();
|
||||
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
|
||||
$itemsAfter = Item::all();
|
||||
|
||||
$this->assertCount(1, $itemsBefore);
|
||||
$this->assertCount(1, $itemsAfter);
|
||||
}
|
||||
|
||||
public function it_should_rate_a_movie($fixture, $shouldHaveRating)
|
||||
{
|
||||
$this->createMovie();
|
||||
|
||||
$api = app($this->apiClass);
|
||||
|
||||
$movieBefore = Item::first();
|
||||
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
|
||||
$movieAfter = Item::first();
|
||||
|
||||
$this->assertEquals(1, $movieBefore->rating);
|
||||
$this->assertEquals($shouldHaveRating, $movieAfter->rating);
|
||||
}
|
||||
|
||||
public function it_should_rate_a_tv_show($fixture, $shouldHaveRating)
|
||||
{
|
||||
$this->createTv();
|
||||
|
||||
$api = app($this->apiClass);
|
||||
|
||||
$tvBefore = Item::first();
|
||||
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
|
||||
$tvAfter = Item::first();
|
||||
|
||||
$this->assertEquals(1, $tvBefore->rating);
|
||||
$this->assertEquals($shouldHaveRating, $tvAfter->rating);
|
||||
}
|
||||
|
||||
public function it_should_mark_an_episode_as_seen($fixture)
|
||||
{
|
||||
$this->createTv();
|
||||
|
||||
$api = app($this->apiClass);
|
||||
|
||||
$seenEpisodesBefore = Episode::where('seen', true)->get();
|
||||
|
||||
$api->handle($this->apiFixtures($fixture));
|
||||
|
||||
$seenEpisodesAfter = Episode::where('seen', true)->get();
|
||||
|
||||
$this->assertCount(0, $seenEpisodesBefore);
|
||||
$this->assertCount(1, $seenEpisodesAfter);
|
||||
}
|
||||
}
|
16
backend/tests/Services/Api/ApiTestInterface.php
Normal file
16
backend/tests/Services/Api/ApiTestInterface.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Services\Api;
|
||||
|
||||
interface ApiTestInterface
|
||||
{
|
||||
public function setUp(): void;
|
||||
public function it_should_abort_the_request();
|
||||
public function it_should_create_a_new_movie();
|
||||
public function it_should_not_create_a_new_movie_if_it_exists();
|
||||
public function it_should_create_a_new_tv_show();
|
||||
public function it_should_not_create_a_new_tv_show_if_it_exists();
|
||||
public function it_should_rate_a_movie();
|
||||
public function it_should_rate_a_tv_show();
|
||||
public function it_should_mark_an_episode_as_seen();
|
||||
}
|
73
backend/tests/Services/Api/FakeApiTest.php
Normal file
73
backend/tests/Services/Api/FakeApiTest.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Services\Api;
|
||||
|
||||
use Tests\Fixtures\FakeApi;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FakeApiTest extends TestCase implements ApiTestInterface
|
||||
{
|
||||
/**
|
||||
* @var ApiTest
|
||||
*/
|
||||
private $apiTest;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->apiTest = app(ApiTest::class);
|
||||
|
||||
$this->apiTest->apiClass = FakeApi::class;
|
||||
|
||||
$this->apiTest->setUp();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_abort_the_request()
|
||||
{
|
||||
$this->apiTest->it_should_abort_the_request('fake/abort.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_create_a_new_movie()
|
||||
{
|
||||
$this->apiTest->it_should_create_a_new_movie('fake/movie.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_not_create_a_new_movie_if_it_exists()
|
||||
{
|
||||
$this->apiTest->it_should_not_create_a_new_movie_if_it_exists('fake/movie.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_create_a_new_tv_show()
|
||||
{
|
||||
$this->apiTest->it_should_create_a_new_tv_show('fake/tv.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_not_create_a_new_tv_show_if_it_exists()
|
||||
{
|
||||
$this->apiTest->it_should_not_create_a_new_tv_show_if_it_exists('fake/tv.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_rate_a_movie()
|
||||
{
|
||||
$this->apiTest->it_should_rate_a_movie('fake/movie_rating.json', 2);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_rate_a_tv_show()
|
||||
{
|
||||
$this->apiTest->it_should_rate_a_tv_show('fake/tv_rating.json', 3);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_mark_an_episode_as_seen()
|
||||
{
|
||||
$this->apiTest->it_should_mark_an_episode_as_seen('fake/episode_seen.json');
|
||||
}
|
||||
}
|
73
backend/tests/Services/Api/PlexApiTest.php
Normal file
73
backend/tests/Services/Api/PlexApiTest.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Services\Api;
|
||||
|
||||
use App\Services\Api\Plex;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PlexApiTest extends TestCase implements ApiTestInterface
|
||||
{
|
||||
/**
|
||||
* @var ApiTest
|
||||
*/
|
||||
private $apiTest;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->apiTest = app(ApiTest::class);
|
||||
|
||||
$this->apiTest->apiClass = Plex::class;
|
||||
|
||||
$this->apiTest->setUp();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_abort_the_request()
|
||||
{
|
||||
$this->apiTest->it_should_abort_the_request('plex/abort.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_create_a_new_movie()
|
||||
{
|
||||
$this->apiTest->it_should_create_a_new_movie('plex/movie.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_not_create_a_new_movie_if_it_exists()
|
||||
{
|
||||
$this->apiTest->it_should_not_create_a_new_movie_if_it_exists('plex/movie.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_create_a_new_tv_show()
|
||||
{
|
||||
$this->apiTest->it_should_create_a_new_tv_show('plex/tv.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_not_create_a_new_tv_show_if_it_exists()
|
||||
{
|
||||
$this->apiTest->it_should_not_create_a_new_tv_show_if_it_exists('plex/tv.json');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_rate_a_movie()
|
||||
{
|
||||
$this->apiTest->it_should_rate_a_movie('plex/movie_rating.json', 2);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_rate_a_tv_show()
|
||||
{
|
||||
$this->apiTest->it_should_rate_a_tv_show('plex/tv_rating.json', 3);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_should_mark_an_episode_as_seen()
|
||||
{
|
||||
$this->apiTest->it_should_mark_an_episode_as_seen('plex/episode_seen.json');
|
||||
}
|
||||
}
|
@ -95,4 +95,22 @@
|
||||
$this->assertEquals(1, $newSettings->daily_reminder);
|
||||
$this->assertEquals(1, $newSettings->weekly_reminder);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function user_can_generate_a_new_api_key()
|
||||
{
|
||||
$apiKeyBefore = $this->user->api_key;
|
||||
|
||||
$this->actingAs($this->user)->patchJson('api/settings/api-key');
|
||||
|
||||
$apiKeyAfter = $this->user->api_key;
|
||||
|
||||
$this->actingAs($this->user)->patchJson('api/settings/api-key');
|
||||
|
||||
$apiKeyAfterSecond = $this->user->api_key;
|
||||
|
||||
$this->assertNull($apiKeyBefore);
|
||||
$this->assertNotNull($apiKeyAfter);
|
||||
$this->assertNotEquals($apiKeyAfterSecond, $apiKeyAfter);
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,9 @@
|
||||
|
||||
trait Factories {
|
||||
|
||||
public function createUser()
|
||||
public function createUser($custom = [])
|
||||
{
|
||||
return factory(User::class)->create();
|
||||
return factory(User::class)->create($custom);
|
||||
}
|
||||
|
||||
public function createSetting()
|
||||
|
@ -24,6 +24,11 @@
|
||||
return collect(json_decode(file_get_contents(__DIR__ . '/../fixtures/flox/' . $type . '.json')))->toArray();
|
||||
}
|
||||
|
||||
protected function apiFixtures($path)
|
||||
{
|
||||
return collect(json_decode(file_get_contents(__DIR__ . '/../fixtures/api/' . $path), true))->toArray();
|
||||
}
|
||||
|
||||
protected function getMovieSrc()
|
||||
{
|
||||
return '/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv';
|
||||
|
73
backend/tests/fixtures/FakeApi.php
vendored
Normal file
73
backend/tests/fixtures/FakeApi.php
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Fixtures;
|
||||
|
||||
use App\Services\Api\Api;
|
||||
|
||||
class FakeApi extends Api
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function abortRequest()
|
||||
{
|
||||
return $this->data['data']['abort'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getType()
|
||||
{
|
||||
return $this->data['data']['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getTitle()
|
||||
{
|
||||
return $this->data['data']['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getRating()
|
||||
{
|
||||
return $this->data['data']['rating'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function shouldRateItem()
|
||||
{
|
||||
return $this->data['data']['rate'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function shouldEpisodeMarkedAsSeen()
|
||||
{
|
||||
return $this->data['data']['seen'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getEpisodeNumber()
|
||||
{
|
||||
return $this->data['data']['episode'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getSeasonNumber()
|
||||
{
|
||||
return $this->data['data']['season'];
|
||||
}
|
||||
}
|
5
backend/tests/fixtures/api/fake/abort.json
vendored
Normal file
5
backend/tests/fixtures/api/fake/abort.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"data": {
|
||||
"abort": true
|
||||
}
|
||||
}
|
12
backend/tests/fixtures/api/fake/episode_seen.json
vendored
Normal file
12
backend/tests/fixtures/api/fake/episode_seen.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"abort": false,
|
||||
"type": "tv",
|
||||
"title": "Game of Thrones",
|
||||
"rate": false,
|
||||
"seen": true,
|
||||
"rating": null,
|
||||
"episode": 2,
|
||||
"season": 1
|
||||
}
|
||||
}
|
12
backend/tests/fixtures/api/fake/movie.json
vendored
Normal file
12
backend/tests/fixtures/api/fake/movie.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"abort": false,
|
||||
"type": "movie",
|
||||
"title": "Warcraft",
|
||||
"rate": false,
|
||||
"seen": false,
|
||||
"rating": null,
|
||||
"episode": null,
|
||||
"season": null
|
||||
}
|
||||
}
|
12
backend/tests/fixtures/api/fake/movie_rating.json
vendored
Normal file
12
backend/tests/fixtures/api/fake/movie_rating.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"abort": false,
|
||||
"type": "movie",
|
||||
"title": "Warcraft",
|
||||
"rate": true,
|
||||
"seen": false,
|
||||
"rating": 2,
|
||||
"episode": null,
|
||||
"season": null
|
||||
}
|
||||
}
|
12
backend/tests/fixtures/api/fake/tv.json
vendored
Normal file
12
backend/tests/fixtures/api/fake/tv.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"abort": false,
|
||||
"type": "tv",
|
||||
"title": "Game of Thrones",
|
||||
"rate": false,
|
||||
"seen": false,
|
||||
"rating": null,
|
||||
"episode": null,
|
||||
"season": null
|
||||
}
|
||||
}
|
12
backend/tests/fixtures/api/fake/tv_rating.json
vendored
Normal file
12
backend/tests/fixtures/api/fake/tv_rating.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"abort": false,
|
||||
"type": "tv",
|
||||
"title": "Game of Thrones",
|
||||
"rate": true,
|
||||
"seen": false,
|
||||
"rating": 3,
|
||||
"episode": null,
|
||||
"season": null
|
||||
}
|
||||
}
|
45
backend/tests/fixtures/api/plex/abort.json
vendored
Normal file
45
backend/tests/fixtures/api/plex/abort.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"event": "media.play",
|
||||
"user": true,
|
||||
"owner": true,
|
||||
"Account": {},
|
||||
"Server": {},
|
||||
"Player": {},
|
||||
"Metadata": {
|
||||
"librarySectionType": "",
|
||||
"ratingKey": "",
|
||||
"key": "",
|
||||
"parentRatingKey": "",
|
||||
"grandparentRatingKey": "",
|
||||
"guid": "",
|
||||
"parentGuid": "",
|
||||
"grandparentGuid": "",
|
||||
"type": "podcast",
|
||||
"title": "a title here",
|
||||
"titleSort": "",
|
||||
"grandparentKey": "",
|
||||
"parentKey": "",
|
||||
"grandparentTitle": "Game of Thrones",
|
||||
"parentTitle": "Season 1",
|
||||
"contentRating": "",
|
||||
"summary": "",
|
||||
"index": 2,
|
||||
"parentIndex": 1,
|
||||
"rating": null,
|
||||
"userRating": null,
|
||||
"viewCount": null,
|
||||
"lastViewedAt": null,
|
||||
"lastRatedAt": null,
|
||||
"year": null,
|
||||
"thumb": "",
|
||||
"art": "",
|
||||
"parentThumb": "",
|
||||
"grandparentThumb": "",
|
||||
"grandparentArt": "",
|
||||
"grandparentTheme": "",
|
||||
"originallyAvailableAt": "",
|
||||
"addedAt": null,
|
||||
"updatedAt": null,
|
||||
"Writer": []
|
||||
}
|
||||
}
|
45
backend/tests/fixtures/api/plex/episode_seen.json
vendored
Normal file
45
backend/tests/fixtures/api/plex/episode_seen.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"event": "media.scrobble",
|
||||
"user": true,
|
||||
"owner": true,
|
||||
"Account": {},
|
||||
"Server": {},
|
||||
"Player": {},
|
||||
"Metadata": {
|
||||
"librarySectionType": "",
|
||||
"ratingKey": "",
|
||||
"key": "",
|
||||
"parentRatingKey": "",
|
||||
"grandparentRatingKey": "",
|
||||
"guid": "",
|
||||
"parentGuid": "",
|
||||
"grandparentGuid": "",
|
||||
"type": "episode",
|
||||
"title": "",
|
||||
"titleSort": "",
|
||||
"grandparentKey": "",
|
||||
"parentKey": "",
|
||||
"grandparentTitle": "Game of Thrones",
|
||||
"parentTitle": "",
|
||||
"contentRating": "",
|
||||
"summary": "",
|
||||
"index": 2,
|
||||
"parentIndex": 1,
|
||||
"rating": null,
|
||||
"userRating": null,
|
||||
"viewCount": null,
|
||||
"lastViewedAt": null,
|
||||
"lastRatedAt": null,
|
||||
"year": null,
|
||||
"thumb": "",
|
||||
"art": "",
|
||||
"parentThumb": "",
|
||||
"grandparentThumb": "",
|
||||
"grandparentArt": "",
|
||||
"grandparentTheme": "",
|
||||
"originallyAvailableAt": "",
|
||||
"addedAt": null,
|
||||
"updatedAt": null,
|
||||
"Writer": []
|
||||
}
|
||||
}
|
43
backend/tests/fixtures/api/plex/movie.json
vendored
Normal file
43
backend/tests/fixtures/api/plex/movie.json
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"event": "media.play",
|
||||
"user": true,
|
||||
"owner": true,
|
||||
"Account": {},
|
||||
"Server": {},
|
||||
"Player": {},
|
||||
"Metadata": {
|
||||
"librarySectionType": "",
|
||||
"ratingKey": "",
|
||||
"key": "",
|
||||
"guid": "",
|
||||
"studio": "",
|
||||
"type": "movie",
|
||||
"title": "Warcraft",
|
||||
"contentRating": "",
|
||||
"summary": "",
|
||||
"rating": null,
|
||||
"audienceRating": null,
|
||||
"userRating": null,
|
||||
"viewCount": null,
|
||||
"lastViewedAt": null,
|
||||
"lastRatedAt": null,
|
||||
"year": null,
|
||||
"tagline": "",
|
||||
"thumb": "",
|
||||
"art": "",
|
||||
"duration": null,
|
||||
"originallyAvailableAt": "",
|
||||
"addedAt": null,
|
||||
"updatedAt": null,
|
||||
"audienceRatingImage": "",
|
||||
"primaryExtraKey": "",
|
||||
"ratingImage": "",
|
||||
"Genre": [],
|
||||
"Director": [],
|
||||
"Writer": [],
|
||||
"Producer": [],
|
||||
"Country": [],
|
||||
"Role": [],
|
||||
"Similar": []
|
||||
}
|
||||
}
|
44
backend/tests/fixtures/api/plex/movie_rating.json
vendored
Normal file
44
backend/tests/fixtures/api/plex/movie_rating.json
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"rating": "",
|
||||
"event": "media.rate",
|
||||
"user": true,
|
||||
"owner": true,
|
||||
"Account": {},
|
||||
"Server": {},
|
||||
"Player": {},
|
||||
"Metadata": {
|
||||
"librarySectionType": "",
|
||||
"ratingKey": "",
|
||||
"key": "",
|
||||
"guid": "",
|
||||
"studio": "",
|
||||
"type": "movie",
|
||||
"title": "Warcraft",
|
||||
"contentRating": "",
|
||||
"summary": "",
|
||||
"rating": null,
|
||||
"audienceRating": null,
|
||||
"userRating": 5,
|
||||
"viewCount": null,
|
||||
"lastViewedAt": null,
|
||||
"lastRatedAt": null,
|
||||
"year": null,
|
||||
"tagline": "",
|
||||
"thumb": "",
|
||||
"art": "",
|
||||
"duration": null,
|
||||
"originallyAvailableAt": "",
|
||||
"addedAt": null,
|
||||
"updatedAt": null,
|
||||
"audienceRatingImage": "",
|
||||
"primaryExtraKey": "",
|
||||
"ratingImage": "",
|
||||
"Genre": [],
|
||||
"Director": [],
|
||||
"Writer": [],
|
||||
"Producer": [],
|
||||
"Country": [],
|
||||
"Role": [],
|
||||
"Similar": []
|
||||
}
|
||||
}
|
45
backend/tests/fixtures/api/plex/tv.json
vendored
Normal file
45
backend/tests/fixtures/api/plex/tv.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"event": "media.play",
|
||||
"user": true,
|
||||
"owner": true,
|
||||
"Account": {},
|
||||
"Server": {},
|
||||
"Player": {},
|
||||
"Metadata": {
|
||||
"librarySectionType": "",
|
||||
"ratingKey": "",
|
||||
"key": "",
|
||||
"parentRatingKey": "",
|
||||
"grandparentRatingKey": "",
|
||||
"guid": "",
|
||||
"parentGuid": "",
|
||||
"grandparentGuid": "",
|
||||
"type": "episode",
|
||||
"title": "",
|
||||
"titleSort": "",
|
||||
"grandparentKey": "",
|
||||
"parentKey": "",
|
||||
"grandparentTitle": "Game of Thrones",
|
||||
"parentTitle": "",
|
||||
"contentRating": "",
|
||||
"summary": "",
|
||||
"index": 2,
|
||||
"parentIndex": 1,
|
||||
"rating": null,
|
||||
"userRating": null,
|
||||
"viewCount": null,
|
||||
"lastViewedAt": null,
|
||||
"lastRatedAt": null,
|
||||
"year": null,
|
||||
"thumb": "",
|
||||
"art": "",
|
||||
"parentThumb": "",
|
||||
"grandparentThumb": "",
|
||||
"grandparentArt": "",
|
||||
"grandparentTheme": "",
|
||||
"originallyAvailableAt": "",
|
||||
"addedAt": null,
|
||||
"updatedAt": null,
|
||||
"Writer": []
|
||||
}
|
||||
}
|
42
backend/tests/fixtures/api/plex/tv_rating.json
vendored
Normal file
42
backend/tests/fixtures/api/plex/tv_rating.json
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"rating": "",
|
||||
"event": "media.rate",
|
||||
"user": true,
|
||||
"owner": true,
|
||||
"Account": {},
|
||||
"Server": {},
|
||||
"Player": {},
|
||||
"Metadata": {
|
||||
"librarySectionType": "",
|
||||
"ratingKey": "",
|
||||
"key": "",
|
||||
"guid": "",
|
||||
"studio": "",
|
||||
"type": "show",
|
||||
"title": "Game of Thrones",
|
||||
"contentRating": "",
|
||||
"summary": "",
|
||||
"index": null,
|
||||
"rating": null,
|
||||
"userRating": 1,
|
||||
"viewCount": null,
|
||||
"lastViewedAt": null,
|
||||
"lastRatedAt": null,
|
||||
"year": null,
|
||||
"thumb": "",
|
||||
"art": "",
|
||||
"banner": "",
|
||||
"theme": "",
|
||||
"duration": null,
|
||||
"originallyAvailableAt": "",
|
||||
"leafCount": null,
|
||||
"viewedLeafCount": null,
|
||||
"childCount": null,
|
||||
"addedAt": null,
|
||||
"updatedAt": null,
|
||||
"Genre": [],
|
||||
"Role": [],
|
||||
"Similar": [],
|
||||
"Location": []
|
||||
}
|
||||
}
|
68
client/app/components/Content/Settings/Api.vue
Normal file
68
client/app/components/Content/Settings/Api.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
|
||||
<div class="settings-box element-ui-checkbox no-select" v-if=" ! loading">
|
||||
<div class="login-error" v-if="config.env === 'demo'"><span>Data cannot be changed in the demo</span></div>
|
||||
|
||||
<form class="login-form" @submit.prevent="generateApiKey()">
|
||||
<span class="update-check">API-Key</span>
|
||||
|
||||
<input type="text" v-model="api_key" readonly>
|
||||
<input type="submit" :value="'Generate new key'">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
import MiscHelper from '../../../helpers/misc';
|
||||
|
||||
import http from 'axios';
|
||||
|
||||
export default {
|
||||
mixins: [MiscHelper],
|
||||
|
||||
created() {
|
||||
this.fetchApiKey();
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: window.config,
|
||||
api_key: '',
|
||||
success: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
loading: state => state.loading
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations([ 'SET_LOADING' ]),
|
||||
|
||||
fetchApiKey() {
|
||||
this.SET_LOADING(true);
|
||||
|
||||
http(`${config.api}/api-key`).then(response => {
|
||||
this.api_key = response.data;
|
||||
|
||||
this.SET_LOADING(false);
|
||||
});
|
||||
},
|
||||
|
||||
generateApiKey() {
|
||||
this.SET_LOADING(true);
|
||||
|
||||
http.patch(`${config.api}/settings/api-key`, {}).then((response) => {
|
||||
this.api_key = response.data;
|
||||
this.SET_LOADING(false);
|
||||
}, error => {
|
||||
alert(error.message);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -9,6 +9,7 @@
|
||||
<span :class="{active: activeTab == 'backup'}" @click="changeActiveTab('backup')">{{ lang('tab backup') }}</span>
|
||||
<span :class="{active: activeTab == 'refresh'}" @click="changeActiveTab('refresh')">{{ lang('refresh') }}</span>
|
||||
<span :class="{active: activeTab == 'reminders'}" @click="changeActiveTab('reminders')">{{ lang('reminders') }}</span>
|
||||
<span :class="{active: activeTab == 'api_key'}" @click="changeActiveTab('api_key')">API</span>
|
||||
</div>
|
||||
|
||||
<span class="loader fullsize-loader" v-if="loading"><i></i></span>
|
||||
@ -19,6 +20,7 @@
|
||||
<misc v-if="activeTab == 'misc'"></misc>
|
||||
<refresh v-if="activeTab == 'refresh'"></refresh>
|
||||
<reminders v-if="activeTab == 'reminders'"></reminders>
|
||||
<api v-if="activeTab == 'api_key'"></api>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
@ -31,6 +33,7 @@
|
||||
import Misc from './Misc.vue';
|
||||
import Refresh from './Refresh.vue';
|
||||
import Reminders from './Reminders.vue';
|
||||
import Api from './Api.vue';
|
||||
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import MiscHelper from '../../../helpers/misc';
|
||||
@ -43,7 +46,7 @@
|
||||
},
|
||||
|
||||
components: {
|
||||
User, Options, Backup, Misc, Refresh, Reminders
|
||||
User, Options, Backup, Misc, Refresh, Reminders, Api
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -70,8 +70,13 @@ main {
|
||||
//@include media(3) { lost-column: 1/5; }
|
||||
@include media(3) { lost-column: 1/3; }
|
||||
//@include media(4) { lost-column: 1/4; }
|
||||
@include media(5) { lost-column: 1/2; }
|
||||
@include media(6) { lost-column: 1; }
|
||||
@include media(5) {
|
||||
lost-column: 1/2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
@include media(6) { }
|
||||
}
|
||||
|
||||
.show-ratings-never {
|
||||
|
@ -1 +0,0 @@
|
||||
Subproject commit 5761befea76867c898e743eb3876c843f6671039
|
26
public/assets/app.css
vendored
26
public/assets/app.css
vendored
@ -2134,7 +2134,17 @@ main {
|
||||
clear: both; } }
|
||||
@media (max-width: 620px) {
|
||||
.item-wrap {
|
||||
width: calc(99.9% * 1/2 - (30px - 30px * 1/2)); }
|
||||
width: calc(99.9% * 1/2 - (30px - 30px * 1/2));
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column; }
|
||||
.item-wrap:nth-child(1n) {
|
||||
float: left;
|
||||
margin-right: 30px;
|
||||
@ -2146,20 +2156,6 @@ main {
|
||||
float: right; }
|
||||
.item-wrap:nth-child(2n + 1) {
|
||||
clear: both; } }
|
||||
@media (max-width: 450px) {
|
||||
.item-wrap {
|
||||
width: calc(99.9% * 1 - (30px - 30px * 1)); }
|
||||
.item-wrap:nth-child(1n) {
|
||||
float: left;
|
||||
margin-right: 30px;
|
||||
clear: none; }
|
||||
.item-wrap:last-child {
|
||||
margin-right: 0; }
|
||||
.item-wrap:nth-child(NaNn) {
|
||||
margin-right: 0;
|
||||
float: right; }
|
||||
.item-wrap:nth-child(NaNn + 1) {
|
||||
clear: both; } }
|
||||
|
||||
.show-ratings-never .rating-0,
|
||||
.show-ratings-never .rating-1,
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user