├── .gitattributes ├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE.md ├── README.md ├── backend ├── .env.example ├── app │ ├── AlternativeTitle.php │ ├── Console │ │ ├── Commands │ │ │ ├── DB.php │ │ │ ├── Daily.php │ │ │ ├── Init.php │ │ │ ├── Refresh.php │ │ │ └── Weekly.php │ │ └── Kernel.php │ ├── Episode.php │ ├── Exceptions │ │ └── Handler.php │ ├── Genre.php │ ├── Http │ │ ├── Controllers │ │ │ ├── ApiController.php │ │ │ ├── CalendarController.php │ │ │ ├── Controller.php │ │ │ ├── ExportImportController.php │ │ │ ├── FileParserController.php │ │ │ ├── GenreController.php │ │ │ ├── HomeController.php │ │ │ ├── ItemController.php │ │ │ ├── SettingController.php │ │ │ ├── SubpageController.php │ │ │ ├── TMDBController.php │ │ │ ├── UserController.php │ │ │ └── VideoController.php │ │ ├── Kernel.php │ │ ├── Middleware │ │ │ ├── EncryptCookies.php │ │ │ ├── HttpBasicAuth.php │ │ │ ├── RedirectIfAuthenticated.php │ │ │ ├── VerifyApiKey.php │ │ │ └── VerifyCsrfToken.php │ │ └── Requests │ │ │ └── .gitkeep │ ├── Item.php │ ├── Jobs │ │ ├── ImportEpisode.php │ │ ├── ImportItem.php │ │ └── UpdateItem.php │ ├── Mail │ │ ├── DailyReminder.php │ │ └── WeeklyReminder.php │ ├── Providers │ │ ├── AppServiceProvider.php │ │ ├── AuthServiceProvider.php │ │ ├── BroadcastServiceProvider.php │ │ ├── EventServiceProvider.php │ │ └── RouteServiceProvider.php │ ├── Services │ │ ├── Api │ │ │ ├── Api.php │ │ │ └── Plex.php │ │ ├── Calendar.php │ │ ├── FileParser.php │ │ ├── IMDB.php │ │ ├── Models │ │ │ ├── AlternativeTitleService.php │ │ │ ├── EpisodeService.php │ │ │ ├── GenreService.php │ │ │ └── ItemService.php │ │ ├── Reminder.php │ │ ├── Storage.php │ │ ├── Subpage.php │ │ └── TMDB.php │ ├── Setting.php │ ├── User.php │ └── helpers.php ├── artisan ├── bootstrap │ ├── app.php │ ├── autoload.php │ └── cache │ │ └── .gitignore ├── composer.json ├── composer.lock ├── config │ ├── app.php │ ├── auth.php │ ├── broadcasting.php │ ├── cache.php │ ├── compile.php │ ├── database.php │ ├── filesystems.php │ ├── hashing.php │ ├── logging.php │ ├── mail.php │ ├── queue.php │ ├── scout.php │ ├── services.php │ ├── session.php │ └── view.php ├── database │ ├── .gitignore │ ├── factories │ │ └── ModelFactory.php │ ├── migrations │ │ ├── 2016_08_01_121249_create_items_table.php │ │ ├── 2016_08_05_132344_create_users_table.php │ │ ├── 2016_10_18_063806_create_settings_table.php │ │ ├── 2016_11_25_090131_create_episodes_table.php │ │ ├── 2016_11_29_110058_add_created_at_field.php │ │ ├── 2016_12_12_074211_add_episode_spoiler_protection_field.php │ │ ├── 2016_12_15_112332_change_original_title_field.php │ │ ├── 2016_12_17_144415_add_src_field_for_episodes.php │ │ ├── 2016_12_18_195039_add_src_field_for_items.php │ │ ├── 2016_12_18_195742_create_alternative_titles_table.php │ │ ├── 2017_01_05_111153_add_last_fetch_to_file_parser_field.php │ │ ├── 2017_01_25_163036_add_subtitles_field_for_episodes_and_items.php │ │ ├── 2017_02_01_122243_change_tmdb_id_field.php │ │ ├── 2017_02_02_080109_add_fp_name_field_to_episodes_and_items.php │ │ ├── 2017_02_22_083139_add_timestamps_to_episodes.php │ │ ├── 2017_02_22_101320_add_timestamps_and_last_seen_at_to_items.php │ │ ├── 2017_02_23_081946_add_release_dates_to_episodes.php │ │ ├── 2017_03_03_075703_add_subpage_fields_to_items_table.php │ │ ├── 2017_07_24_180742_add_watchlist_field_to_items_tabe.php │ │ ├── 2017_07_25_095610_add_show_watchlist_everywhere_field.php │ │ ├── 2017_08_15_082335_add_show_ratings_field.php │ │ ├── 2017_12_29_222510_create_genres_table.php │ │ ├── 2017_12_29_222633_create_genre_item_table.php │ │ ├── 2018_03_16_235550_add_refreshed_at_field.php │ │ ├── 2018_04_29_162253_create_jobs_table.php │ │ ├── 2018_04_29_164313_create_failed_jobs_table.php │ │ ├── 2018_11_10_180540_add_refresh_automatically_field.php │ │ ├── 2018_11_10_180653_add_reminders_send_to_field.php │ │ ├── 2018_11_10_180828_add_daily_and_weekly_fields.php │ │ ├── 2018_12_28_230635_add_homepage_field_to_items_table.php │ │ ├── 2019_12_23_122213_add_api_key_to_users.php │ │ ├── 2019_12_24_104600_add_released_timestamp_to_items.php │ │ ├── 2020_01_09_210708_change_released_timestamp_to_datetime.php │ │ └── 2020_01_27_105656_drop_tmdb_id_unique_index.php │ └── seeds │ │ ├── .gitkeep │ │ └── DatabaseSeeder.php ├── phpunit.xml ├── routes │ ├── api.php │ ├── console.php │ └── web.php ├── server.php ├── storage │ ├── app │ │ ├── .gitignore │ │ └── public │ │ │ └── .gitignore │ ├── framework │ │ ├── .gitignore │ │ ├── cache │ │ │ └── .gitignore │ │ ├── sessions │ │ │ └── .gitignore │ │ └── views │ │ │ └── .gitignore │ └── logs │ │ └── .gitignore └── tests │ ├── ApplicationTest.php │ ├── CreatesApplication.php │ ├── Services │ ├── AlternativeTitleServiceTest.php │ ├── Api │ │ ├── ApiTest.php │ │ ├── ApiTestInterface.php │ │ ├── FakeApiTest.php │ │ └── PlexApiTest.php │ ├── CalendarTest.php │ ├── EpisodeServiceTest.php │ ├── FileParserTest.php │ ├── GenreServiceTest.php │ ├── IMDBTest.php │ ├── ItemServiceTest.php │ └── TMDBTest.php │ ├── Setting │ ├── ExportImportTest.php │ ├── SettingTest.php │ └── UserTest.php │ ├── TestCase.php │ ├── Traits │ ├── Factories.php │ ├── Fixtures.php │ └── Mocks.php │ ├── api │ └── VideoTest.php │ └── fixtures │ ├── FakeApi.php │ ├── api │ ├── fake │ │ ├── abort.json │ │ ├── episode_seen.json │ │ ├── movie.json │ │ ├── movie_rating.json │ │ ├── tv.json │ │ └── tv_rating.json │ └── plex │ │ ├── abort.json │ │ ├── episode_seen.json │ │ ├── movie.json │ │ ├── movie_rating.json │ │ ├── tv.json │ │ └── tv_rating.json │ ├── flox │ ├── export-new-version.json │ ├── export.json │ ├── movie.json │ ├── tv.json │ └── wrong-file.txt │ ├── fp │ ├── all.json │ ├── movie │ │ ├── added.json │ │ ├── added_not_found.json │ │ ├── removed.json │ │ ├── unknown.json │ │ ├── updated.json │ │ ├── updated_found.json │ │ ├── updated_is_empty.json │ │ └── updated_not_found.json │ └── tv │ │ ├── added.json │ │ ├── added_not_found.json │ │ ├── removed.json │ │ ├── unknown.json │ │ ├── updated.json │ │ ├── updated_found.json │ │ ├── updated_is_empty.json │ │ ├── updated_not_found.json │ │ └── updated_one.json │ ├── imdb │ ├── rating.txt │ ├── with-rating.html │ └── without-rating.html │ ├── media │ ├── 1.mp4 │ └── 2.mp4 │ └── tmdb │ ├── empty.json │ ├── movie │ ├── alternative_titles.json │ ├── details-failing.json │ ├── details.json │ ├── genres.json │ ├── movie.json │ ├── search.json │ ├── trending.json │ └── upcoming.json │ ├── multi.json │ ├── tv │ ├── alternative_titles.json │ ├── details.json │ ├── episodes.json │ ├── genres.json │ ├── search.json │ ├── search_for_the_office.json │ ├── trending.json │ └── tv.json │ └── videos.json ├── bin └── install_worker_service.sh ├── client ├── .babelrc ├── app │ ├── app.js │ ├── components │ │ ├── Content │ │ │ ├── Calendar.vue │ │ │ ├── Content.vue │ │ │ ├── Item.vue │ │ │ ├── SearchContent.vue │ │ │ ├── Settings │ │ │ │ ├── Api.vue │ │ │ │ ├── Backup.vue │ │ │ │ ├── Index.vue │ │ │ │ ├── Misc.vue │ │ │ │ ├── Options.vue │ │ │ │ ├── Refresh.vue │ │ │ │ ├── Reminders.vue │ │ │ │ └── User.vue │ │ │ ├── Subpage.vue │ │ │ └── TMDBContent.vue │ │ ├── Footer.vue │ │ ├── Header.vue │ │ ├── Login.vue │ │ ├── Modal │ │ │ ├── Index.vue │ │ │ ├── Season.vue │ │ │ └── Trailer.vue │ │ ├── Rating.vue │ │ └── Search.vue │ ├── config.js │ ├── helpers │ │ ├── item.js │ │ └── misc.js │ ├── routes.js │ └── store │ │ ├── actions.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── types.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── resources │ ├── app.blade.php │ ├── languages │ │ ├── ar.json │ │ ├── da.json │ │ ├── de.json │ │ ├── el.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── nl.json │ │ ├── pt-br.json │ │ └── ru.json │ ├── mails │ │ ├── compiled │ │ │ ├── daily.blade.php │ │ │ └── weekly.blade.php │ │ └── templates │ │ │ ├── daily.mjml.blade.php │ │ │ └── weekly.mjml.blade.php │ └── sass │ │ ├── _base.scss │ │ ├── _element-ui.scss │ │ ├── _misc.scss │ │ ├── _normalize.scss │ │ ├── _shake.scss │ │ ├── _sprite.scss │ │ ├── app.scss │ │ └── components │ │ ├── _calendar.scss │ │ ├── _content.scss │ │ ├── _footer.scss │ │ ├── _header.scss │ │ ├── _lists.scss │ │ ├── _login.scss │ │ ├── _modal.scss │ │ ├── _search.scss │ │ └── _subpage.scss └── webpack.config.js └── public ├── .htaccess ├── assets ├── app.css ├── app.js ├── backdrop │ └── .gitkeep ├── favicon.ico ├── img │ ├── add.png │ ├── clock.png │ ├── close.png │ ├── github.png │ ├── hamburger.png │ ├── has-src.png │ ├── is-finished.png │ ├── logo-login.png │ ├── logo-small-light.png │ ├── logo-small.png │ ├── logo.png │ ├── no-image-subpage.png │ ├── no-image.png │ ├── rating-0.png │ ├── rating-1.png │ ├── rating-2.png │ ├── rating-3.png │ ├── search-dark.png │ ├── search.png │ ├── seen-active.png │ ├── seen.png │ ├── suggest.png │ ├── tmdb.png │ ├── trailer.png │ ├── watchlist-remove.png │ └── watchlist.png ├── poster │ ├── .gitkeep │ └── subpage │ │ └── .gitkeep ├── screenshot.jpg └── vendor.js ├── exports └── .gitkeep ├── index.php └── web.config /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /backend/vendor 3 | /.idea 4 | Homestead.json 5 | Homestead.yaml 6 | /backend/.env 7 | /bin/.worker.pid 8 | /backend/.phpunit.result.cache 9 | 10 | */.DS_Store 11 | /public/assets/app.css 12 | /public/assets/app.js 13 | /public/assets/vendor.js 14 | /public/assets/poster/*.jpg 15 | /public/assets/poster/subpage/*.jpg 16 | /public/assets/backdrop/*.jpg 17 | /public/exports/*.json 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "flox-file-parser"] 2 | path = flox-file-parser 3 | url = https://github.com/exane/flox-file-parser 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | php: 6 | - 7.2 7 | 8 | env: 9 | APP_ENV: testing 10 | CACHE_DRIVER: array 11 | SESSION_DRIVER: array 12 | QUEUE_DRIVER: sync 13 | APP_KEY: 16efa6c23c2e8c705826b0e66778fbe7 14 | DB_CONNECTION: sqlite 15 | 16 | cache: 17 | directories: 18 | - backend/vendor 19 | 20 | services: 21 | - mysql 22 | 23 | script: 24 | - (cd backend && vendor/bin/phpunit --testdox) 25 | 26 | install: 27 | - (cd backend && composer install --prefer-source --no-interaction) 28 | 29 | notifications: 30 | email: false 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 devfake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | TMDB_API_KEY= 2 | TRANSLATION= 3 | 4 | CLIENT_URI=/ 5 | LOADING_ITEMS=30 6 | 7 | # Default 10 minutes (600 seconds) 8 | PHP_TIME_LIMIT=600 9 | 10 | # Set your correct timezone 11 | TIMEZONE=UTC 12 | 13 | # Date pattern for reminder mails 14 | DATE_FORMAT_PATTERN=d.m.Y 15 | 16 | DAILY_REMINDER_TIME=10:00 17 | WEEKLY_REMINDER_TIME=20:00 18 | 19 | DB_CONNECTION=mysql 20 | DB_HOST=localhost 21 | DB_PORT=3306 22 | DB_DATABASE= 23 | DB_USERNAME= 24 | DB_PASSWORD= 25 | 26 | APP_URL=http://localhost 27 | APP_ENV=local 28 | APP_KEY= 29 | APP_DEBUG=true 30 | 31 | APP_LOG=daily 32 | QUEUE_DRIVER=database 33 | 34 | REDIS_HOST=127.0.0.1 35 | REDIS_PASSWORD=null 36 | REDIS_PORT=6379 37 | 38 | MAIL_DRIVER=smtp 39 | MAIL_HOST=smtp.mailtrap.io 40 | MAIL_PORT=2525 41 | MAIL_USERNAME=null 42 | MAIL_PASSWORD=null 43 | MAIL_ENCRYPTION=null 44 | -------------------------------------------------------------------------------- /backend/app/AlternativeTitle.php: -------------------------------------------------------------------------------- 1 | firstOrCreate([ 33 | 'title' => $title->title, 34 | 'tmdb_id' => $tmdbId, 35 | 'country' => $title->iso_3166_1, 36 | ]); 37 | } 38 | } 39 | 40 | /* 41 | * Scope to find the result via tmdb_id. 42 | */ 43 | public function scopeFindByTmdbId($query, $tmdbId) 44 | { 45 | return $query->where('tmdb_id', $tmdbId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/app/Console/Commands/DB.php: -------------------------------------------------------------------------------- 1 | genreService = $genreService; 22 | } 23 | 24 | public function handle() 25 | { 26 | if($this->option('fresh')) { 27 | $this->alert('ALL DATA WILL BE REMOVED'); 28 | } 29 | 30 | try { 31 | $this->createMigrations(); 32 | } catch(\Exception $e) { 33 | $this->error('Can not connect to the database. Error: ' . $e->getMessage()); 34 | $this->error('Make sure your database credentials in .env are correct'); 35 | 36 | return false; 37 | } 38 | 39 | $this->createUser(); 40 | } 41 | 42 | private function createMigrations() 43 | { 44 | $this->info('TRYING TO MIGRATE DATABASE'); 45 | 46 | if($this->option('fresh')) { 47 | $this->call('migrate:fresh', ['--force' => true]); 48 | } else { 49 | $this->call('migrate', ['--force' => true]); 50 | } 51 | 52 | $this->info('MIGRATION COMPLETED'); 53 | } 54 | 55 | private function createUser() 56 | { 57 | $username = $this->ask('Enter your admin username', $this->argument("username")); 58 | $password = $this->ask('Enter your admin password', $this->argument("password")); 59 | 60 | if($this->option('fresh')) { 61 | LaravelDB::table('users')->delete(); 62 | } 63 | 64 | $user = new User(); 65 | $user->username = $username; 66 | $user->password = bcrypt($password); 67 | $user->save(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /backend/app/Console/Commands/Daily.php: -------------------------------------------------------------------------------- 1 | reminder = $reminder; 20 | } 21 | 22 | public function handle() 23 | { 24 | $this->reminder->sendDaily(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/app/Console/Commands/Init.php: -------------------------------------------------------------------------------- 1 | createENVFile(); 15 | $this->fillDatabaseCredentials(); 16 | $this->setAppKey(); 17 | } 18 | 19 | private function createENVFile() 20 | { 21 | if( ! file_exists('.env')) { 22 | $this->info('CREATING .ENV FILE'); 23 | copy('.env.example', '.env'); 24 | } 25 | } 26 | 27 | private function fillDatabaseCredentials() 28 | { 29 | $value = $this->ask('Enter your Database Name', $this->argument("database")); 30 | $this->changeENV('DB_DATABASE', $value); 31 | 32 | $value = $this->ask('Enter your Database Username', $this->argument("username")); 33 | $this->changeENV('DB_USERNAME', $value); 34 | 35 | $value = $this->ask('Enter your Database Password', $this->argument("password")); 36 | $this->changeENV('DB_PASSWORD', $value); 37 | 38 | $value = $this->ask('Enter your Database Hostname', $this->argument("hostname")); 39 | $this->changeENV('DB_HOST', $value); 40 | 41 | $value = $this->ask('Enter your Database Port', $this->argument("port")); 42 | $this->changeENV('DB_PORT', $value); 43 | } 44 | 45 | private function setAppKey() 46 | { 47 | if( ! env('APP_KEY')) { 48 | $this->info('GENERATING APP KEY'); 49 | $this->callSilent('key:generate'); 50 | } 51 | } 52 | 53 | private function changeENV($type, $value) 54 | { 55 | file_put_contents('.env', preg_replace( 56 | "/$type=.*/", 57 | $type . '=' . $value, 58 | file_get_contents('.env') 59 | )); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/app/Console/Commands/Refresh.php: -------------------------------------------------------------------------------- 1 | itemService = $itemService; 20 | } 21 | 22 | public function handle() 23 | { 24 | $this->itemService->refreshAll(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/app/Console/Commands/Weekly.php: -------------------------------------------------------------------------------- 1 | reminder = $reminder; 20 | } 21 | 22 | public function handle() 23 | { 24 | $this->reminder->sendWeekly(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | runningUnitTests()) { 37 | return null; 38 | } 39 | 40 | if (Schema::hasTable('settings')) { 41 | $settings = Setting::first(); 42 | 43 | if ($settings->refresh_automatically) { 44 | $schedule->command(Refresh::class)->dailyAt('06:00'); 45 | } 46 | 47 | if ($settings->daily_reminder) { 48 | $schedule->command(Daily::class)->dailyAt(config('app.DAILY_REMINDER_TIME')); 49 | } 50 | 51 | if ($settings->weekly_reminder) { 52 | $schedule->command(Weekly::class)->sundays()->at(config('app.WEEKLY_REMINDER_TIME')); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Register the Closure based commands for the application. 59 | * 60 | * @return void 61 | */ 62 | protected function commands() 63 | { 64 | require base_path('routes/console.php'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/app/Genre.php: -------------------------------------------------------------------------------- 1 | where('name', $genre); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | plex = $plex; 17 | } 18 | 19 | public function plex() 20 | { 21 | $payload = json_decode(request('payload'), true); 22 | 23 | $this->plex->handle($payload); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/CalendarController.php: -------------------------------------------------------------------------------- 1 | calendar = $calendar; 14 | } 15 | 16 | public function items() 17 | { 18 | return $this->calendar->items(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 19 | } 20 | 21 | /** 22 | * Call flox-file-parser. 23 | * 24 | * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response 25 | */ 26 | public function call() 27 | { 28 | try { 29 | $this->parser->fetch(); 30 | } catch(ConnectException $e) { 31 | return response("Can't connect to file-parser. Make sure the server is running.", Response::HTTP_NOT_FOUND); 32 | } catch(\Exception $e) { 33 | return response("Error in file-parser:" . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); 34 | } 35 | } 36 | 37 | /** 38 | * Will be called from flox-file-parser itself. 39 | * 40 | * @param Request $request 41 | * @return \Illuminate\Http\JsonResponse 42 | */ 43 | public function receive(Request $request) 44 | { 45 | logInfo("FileParserController.receive called"); 46 | $content = json_decode($request->getContent()); 47 | 48 | return $this->updateDatabase($content); 49 | } 50 | 51 | /** 52 | * @return mixed 53 | */ 54 | public function lastFetched() 55 | { 56 | return $this->parser->lastFetched(); 57 | } 58 | 59 | /** 60 | * @param $files 61 | * @return \Illuminate\Http\JsonResponse 62 | */ 63 | private function updateDatabase($files) 64 | { 65 | return $this->parser->updateDatabase($files); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/GenreController.php: -------------------------------------------------------------------------------- 1 | genreService = $genreService; 16 | $this->genre = $genre; 17 | } 18 | 19 | public function allGenres() 20 | { 21 | return $this->genre->all(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | parseLanguage(); 12 | 13 | return view('app') 14 | ->withLang($language); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/SubpageController.php: -------------------------------------------------------------------------------- 1 | item($tmdbId, $mediaType); 13 | } 14 | 15 | public function imdbRating($id, IMDB $imdb) 16 | { 17 | return $imdb->parseRating($id); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/TMDBController.php: -------------------------------------------------------------------------------- 1 | tmdb = $tmdb; 16 | } 17 | 18 | public function search() 19 | { 20 | return $this->tmdb->search(Request::input('q')); 21 | } 22 | 23 | public function suggestions($tmdbId, $mediaType) 24 | { 25 | return $this->tmdb->suggestions($mediaType, $tmdbId); 26 | } 27 | 28 | public function genre($genre) 29 | { 30 | return $this->tmdb->byGenre($genre); 31 | } 32 | 33 | public function trending() 34 | { 35 | return $this->tmdb->trending(); 36 | } 37 | 38 | public function nowPlaying() 39 | { 40 | return $this->tmdb->nowPlaying(); 41 | } 42 | 43 | public function upcoming() 44 | { 45 | return $this->tmdb->upcoming(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/app/Http/Controllers/VideoController.php: -------------------------------------------------------------------------------- 1 | tv = $tv; 17 | $this->movie = $movie; 18 | } 19 | 20 | public function serve($type, $id) 21 | { 22 | try { 23 | $src = $this->getSrc($type, $id); 24 | 25 | if( ! $src) { 26 | throw new \Exception('No src file for id "' . $id . '"'); 27 | } 28 | 29 | if ( ! File::exists($src)) { 30 | throw new \Exception('File does not exist: ' . $src); 31 | } 32 | } catch (\Exception $e) { 33 | return response('File not found: ' . $e->getMessage(), 404); 34 | } 35 | 36 | return response()->file($src, [ 37 | 'Content-Type' => 'video/mp4', 38 | 'Accept-Ranges' => 'bytes', 39 | ]); 40 | } 41 | 42 | private function getSrc($type, $id) 43 | { 44 | if ($type != 'tv' && $type != 'movie') { 45 | throw new \Exception('Unknown type "' . $type . '" in route'); 46 | } 47 | 48 | return $this->{$type}->findOrFail($id)->src; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 28 | \App\Http\Middleware\EncryptCookies::class, 29 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 30 | \Illuminate\Session\Middleware\StartSession::class, 31 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 32 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 33 | ], 34 | 35 | 'api' => [ 36 | \App\Http\Middleware\EncryptCookies::class, 37 | 'throttle:60,1', 38 | 'bindings', 39 | ], 40 | ]; 41 | 42 | /** 43 | * The application's route middleware. 44 | * 45 | * These middleware may be assigned to groups or used individually. 46 | * 47 | * @var array 48 | */ 49 | protected $routeMiddleware = [ 50 | 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 51 | 'auth.basic' => \App\Http\Middleware\HttpBasicAuth::class, 52 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 53 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 54 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 55 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 56 | 'csrf' => \App\Http\Middleware\VerifyCsrfToken::class, 57 | 'api_key' => VerifyApiKey::class, 58 | ]; 59 | } 60 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | $request->getUser(), 'password' => $request->getPassword()])) { 20 | return $next($request); 21 | } 22 | 23 | return response('Invalid credentials.', Response::HTTP_UNAUTHORIZED, ['WWW-Authenticate' => 'Basic']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 21 | return redirect('/home'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/VerifyApiKey.php: -------------------------------------------------------------------------------- 1 | user = $user; 20 | } 21 | 22 | /** 23 | * Handle an incoming request. 24 | * 25 | * @param \Illuminate\Http\Request $request 26 | * @param \Closure $next 27 | * @return mixed 28 | */ 29 | public function handle($request, Closure $next) 30 | { 31 | if (!$request->token) { 32 | return response(['message' => 'No token provided'], Response::HTTP_UNAUTHORIZED); 33 | } 34 | 35 | if (!$this->user->findByApiKey($request->token)->exists()) { 36 | return response(['message' => 'No valid token provided'], Response::HTTP_UNAUTHORIZED); 37 | } 38 | 39 | return $next($request); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | episodes = json_decode($episodes); 26 | } 27 | 28 | /** 29 | * Execute the job. 30 | * 31 | * @param Episode $episode 32 | * @return void 33 | * 34 | * @throws \Exception 35 | */ 36 | public function handle(Episode $episode) 37 | { 38 | foreach($this->episodes as $ep) { 39 | logInfo("Importing episode", [$ep->name]); 40 | try { 41 | $ep = collect($ep)->except('id')->toArray(); 42 | 43 | $episode->create($ep); 44 | } catch(\Exception $e) { 45 | logInfo("Failed:", [$e]); 46 | throw $e; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/app/Jobs/ImportItem.php: -------------------------------------------------------------------------------- 1 | item = json_decode($item); 26 | } 27 | 28 | /** 29 | * Execute the job. 30 | * 31 | * @param ItemService $itemService 32 | * 33 | * @return void 34 | * 35 | * @throws \Exception 36 | */ 37 | public function handle(ItemService $itemService) 38 | { 39 | try { 40 | $itemService->import($this->item); 41 | } catch(\Exception $e) { 42 | logInfo("Failed:", [$e]); 43 | throw $e; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/app/Jobs/UpdateItem.php: -------------------------------------------------------------------------------- 1 | itemId = $itemId; 26 | } 27 | 28 | /** 29 | * Execute the job. 30 | * 31 | * @param ItemService $itemService 32 | * 33 | * @return void 34 | * 35 | * @throws \Exception 36 | */ 37 | public function handle(ItemService $itemService) 38 | { 39 | try { 40 | $itemService->refresh($this->itemId); 41 | } catch (\Exception $e) { 42 | logInfo("Failed:", [$e]); 43 | throw $e; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/app/Mail/DailyReminder.php: -------------------------------------------------------------------------------- 1 | episodes = $episodes; 26 | $this->movies = $movies; 27 | } 28 | 29 | /** 30 | * Build the message. 31 | * 32 | * @param Storage $storage 33 | * 34 | * @return $this 35 | */ 36 | public function build(Storage $storage) 37 | { 38 | $lang = collect(json_decode($storage->parseLanguage())); 39 | $headline = $lang['daily reminder']; 40 | $date = date(config('app.DATE_FORMAT_PATTERN')); 41 | 42 | return $this->view('mails.compiled.daily')->with([ 43 | 'headline' => $headline, 44 | 'episodesHeadline' => $lang['episodes today'], 45 | 'moviesHeadline' => $lang['movies today'], 46 | 'episodes' => $this->episodes, 47 | 'movies' => $this->movies, 48 | 'date' => $date, 49 | ])->subject("$headline $date"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /backend/app/Mail/WeeklyReminder.php: -------------------------------------------------------------------------------- 1 | episodes = $episodes; 30 | $this->movies = $movies; 31 | $this->startWeek = date(config('app.DATE_FORMAT_PATTERN'), $startWeek); 32 | $this->endWeek = date(config('app.DATE_FORMAT_PATTERN'), $endWeek); 33 | } 34 | 35 | /** 36 | * Build the message. 37 | * 38 | * @param Storage $storage 39 | * 40 | * @return $this 41 | */ 42 | public function build(Storage $storage) 43 | { 44 | $lang = collect(json_decode($storage->parseLanguage())); 45 | $headline = $lang['weekly reminder']; 46 | $date = $this->startWeek . ' - ' . $this->endWeek; 47 | 48 | return $this->view('mails.compiled.weekly')->with([ 49 | 'headline' => $headline, 50 | 'episodes' => $this->episodes, 51 | 'movies' => $this->movies, 52 | 'episodesHeadline' => $lang['episodes'], 53 | 'moviesHeadline' => $lang['movies'], 54 | 'date' => $date, 55 | ])->subject("$headline $date"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | id === (int) $userId; 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'App\Listeners\EventListener', 18 | ], 19 | ]; 20 | 21 | /** 22 | * Register any events for your application. 23 | * 24 | * @return void 25 | */ 26 | public function boot() 27 | { 28 | parent::boot(); 29 | 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapApiRoutes(); 39 | 40 | $this->mapWebRoutes(); 41 | 42 | // 43 | } 44 | 45 | /** 46 | * Define the "web" routes for the application. 47 | * 48 | * These routes all receive session state, CSRF protection, etc. 49 | * 50 | * @return void 51 | */ 52 | protected function mapWebRoutes() 53 | { 54 | Route::group([ 55 | 'middleware' => 'web', 56 | 'namespace' => $this->namespace, 57 | ], function ($router) { 58 | require base_path('routes/web.php'); 59 | }); 60 | } 61 | 62 | /** 63 | * Define the "api" routes for the application. 64 | * 65 | * These routes are typically stateless. 66 | * 67 | * @return void 68 | */ 69 | protected function mapApiRoutes() 70 | { 71 | Route::group([ 72 | 'middleware' => 'api', 73 | 'namespace' => $this->namespace, 74 | 'prefix' => 'api', 75 | ], function ($router) { 76 | require base_path('routes/api.php'); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /backend/app/Services/Api/Plex.php: -------------------------------------------------------------------------------- 1 | data['Metadata']['type'], ['episode', 'show', 'movie']); 14 | } 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | protected function getType() 20 | { 21 | $type = $this->data['Metadata']['type']; 22 | 23 | return in_array($type, ['episode', 'show']) ? 'tv' : 'movie'; 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | protected function getTitle() 30 | { 31 | return $this->data['Metadata']['grandparentTitle'] ?? $this->data['Metadata']['title']; 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | protected function shouldRateItem() 38 | { 39 | $type = $this->data['Metadata']['type']; 40 | 41 | // Flox has no ratings for seasons or episodes. 42 | return in_array($type, ['show', 'movie']) && $this->data['event'] === 'media.rate'; 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | protected function getRating() 49 | { 50 | $rating = $this->data['Metadata']['userRating']; 51 | 52 | if ($rating > 7) { 53 | return 1; 54 | } 55 | 56 | if ($rating > 4) { 57 | return 2; 58 | } 59 | 60 | return 3; 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | protected function shouldEpisodeMarkedAsSeen() 67 | { 68 | return $this->data['event'] === 'media.scrobble' && $this->getType() === 'tv'; 69 | } 70 | 71 | /** 72 | * @inheritDoc 73 | */ 74 | protected function getEpisodeNumber() 75 | { 76 | return $this->data['Metadata']['index'] ?? null; 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | protected function getSeasonNumber() 83 | { 84 | return $this->data['Metadata']['parentIndex'] ?? null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /backend/app/Services/Calendar.php: -------------------------------------------------------------------------------- 1 | whereHas('calendarItem') 20 | //->whereBetween('release_episode', [today()->subDays(7)->timestamp, today()->addDays(7)->timestamp]) 21 | ->get(['id', 'tmdb_id', 'release_episode', 'season_number', 'episode_number']); 22 | 23 | $movies = Item::where('media_type', 'movie') 24 | //->whereBetween('released', [today()->subDays(7)->timestamp, today()->addDays(70)->timestamp]) 25 | ->get(); 26 | 27 | $episodesFormatted = $episodes->map(function($episode) { 28 | return $this->buildEvent($episode, 'tv'); 29 | }); 30 | 31 | $moviesFormatted = $movies->map(function($movie) { 32 | return $this->buildEvent($movie, 'movies'); 33 | }); 34 | 35 | return collect($moviesFormatted)->merge($episodesFormatted); 36 | } 37 | 38 | private function buildEvent($item, $type) 39 | { 40 | return [ 41 | 'startDate' => $item->startDate, 42 | 'id' => $item->id, 43 | 'tmdb_id' => $item->tmdb_id, 44 | 'type' => $type, 45 | 'classes' => "$type watchlist-{$this->isOnWatchlist($item, $type)}", 46 | 'title' => $this->buildTitle($item, $type), 47 | ]; 48 | } 49 | 50 | private function isOnWatchlist($item, $type) 51 | { 52 | if($type === 'tv') { 53 | return $item->calendarItem->watchlist; 54 | } 55 | 56 | return $item->watchlist; 57 | } 58 | 59 | private function buildTitle($item, $type) 60 | { 61 | if($type === 'tv') { 62 | return $item->calendarItem->title . ' ' . 'S' . $item->season_number . 'E' . $item->episode_number; 63 | } 64 | 65 | return $item->title; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /backend/app/Services/IMDB.php: -------------------------------------------------------------------------------- 1 | document = $document; 14 | } 15 | 16 | public function parseRating($id = null) 17 | { 18 | $document = $this->document->loadHtmlFile(config('services.imdb.url') . $id); 19 | 20 | // We don't need to check if we found a result if we loop over them. 21 | foreach($document->find('.ratingValue strong span') as $rating) { 22 | return $rating->text(); 23 | } 24 | 25 | return null; 26 | } 27 | } -------------------------------------------------------------------------------- /backend/app/Services/Models/AlternativeTitleService.php: -------------------------------------------------------------------------------- 1 | model = $model; 23 | $this->item = $item; 24 | $this->tmdb = $tmdb; 25 | } 26 | 27 | /** 28 | * @param $item 29 | */ 30 | public function create($item) 31 | { 32 | $titles = $this->tmdb->getAlternativeTitles($item); 33 | 34 | $this->model->store($titles, $item->tmdb_id); 35 | } 36 | 37 | /** 38 | * Remove all titles by tmdb_id. 39 | * 40 | * @param $tmdbId 41 | */ 42 | public function remove($tmdbId) 43 | { 44 | $this->model->where('tmdb_id', $tmdbId)->delete(); 45 | } 46 | 47 | /** 48 | * Update alternative titles for all tv shows and movies. 49 | * For old versions of flox (<= 1.2.2) or to keep all alternative titles up to date. 50 | */ 51 | public function update() 52 | { 53 | increaseTimeLimit(); 54 | 55 | $items = $this->item->all(); 56 | 57 | $items->each(function($item) { 58 | $this->create($item); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/app/Services/Models/GenreService.php: -------------------------------------------------------------------------------- 1 | model = $model; 21 | $this->tmdb = $tmdb; 22 | } 23 | 24 | /** 25 | * Sync the pivot table genre_item. 26 | * 27 | * @param $item 28 | * @param $ids 29 | */ 30 | public function sync($item, $ids) 31 | { 32 | $item->genre()->sync($ids); 33 | } 34 | 35 | /** 36 | * Update the genres table. 37 | */ 38 | public function updateGenreLists() 39 | { 40 | $genres = $this->tmdb->getGenreLists(); 41 | 42 | DB::beginTransaction(); 43 | 44 | foreach($genres as $mediaType) { 45 | foreach($mediaType->genres as $genre) { 46 | $this->model->firstOrCreate( 47 | ['id' => $genre->id], 48 | ['name' => $genre->name] 49 | ); 50 | } 51 | } 52 | 53 | DB::commit(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /backend/app/Services/Reminder.php: -------------------------------------------------------------------------------- 1 | startOfDay()->timestamp; 22 | $endToday = today()->endOfDay()->timestamp; 23 | 24 | $episodes = Episode::whereBetween('release_episode', [$startToday, $endToday]) 25 | ->with('item') 26 | ->orderBy('tmdb_id') 27 | ->get(); 28 | 29 | $movies = Item::where('media_type', 'movie') 30 | ->whereBetween('released', [$startToday, $endToday]) 31 | ->get(); 32 | 33 | if (count($episodes) || count($movies)) { 34 | Mail::to($settings->reminders_send_to) 35 | ->send(new DailyReminder($episodes, $movies)); 36 | } 37 | } 38 | 39 | /** 40 | * Send a weekly summary. 41 | */ 42 | public function sendWeekly() 43 | { 44 | $settings = Setting::first(); 45 | 46 | $startWeek = today()->startOfWeek()->timestamp; 47 | $endWeek = today()->endOfWeek()->timestamp; 48 | 49 | $episodes = Episode::whereBetween('release_episode', [$startWeek, $endWeek]) 50 | ->with('item') 51 | ->orderBy('tmdb_id') 52 | ->get(); 53 | 54 | $movies = Item::where('media_type', 'movie') 55 | ->whereBetween('released', [$startWeek, $endWeek]) 56 | ->get(); 57 | 58 | if (count($episodes) || count($movies)) { 59 | Mail::to($settings->reminders_send_to) 60 | ->send(new WeeklyReminder($episodes, $movies, $startWeek, $endWeek)); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/app/Services/Storage.php: -------------------------------------------------------------------------------- 1 | put($file, $items); 18 | } 19 | 20 | /** 21 | * Create the export filename. 22 | * 23 | * @return string 24 | */ 25 | public function createExportFilename() 26 | { 27 | return 'flox--' . date('Y-m-d---H-i') . '.json'; 28 | } 29 | 30 | /** 31 | * Download poster and backdrop image files. 32 | * 33 | * @param $poster 34 | * @param $backdrop 35 | */ 36 | public function downloadImages($poster, $backdrop) 37 | { 38 | if($poster) { 39 | LaravelStorage::put($poster, file_get_contents(config('services.tmdb.poster') . $poster)); 40 | LaravelStorage::disk('subpage')->put($poster, file_get_contents(config('services.tmdb.poster_subpage') . $poster)); 41 | } 42 | 43 | if($backdrop) { 44 | LaravelStorage::disk('backdrop')->put($backdrop, file_get_contents(config('services.tmdb.backdrop') . $backdrop)); 45 | } 46 | } 47 | 48 | /** 49 | * Delete poster and backdrop image files. 50 | * 51 | * @param $poster 52 | * @param $backdrop 53 | */ 54 | public function removeImages($poster, $backdrop) 55 | { 56 | LaravelStorage::delete($poster); 57 | LaravelStorage::disk('subpage')->delete($poster); 58 | LaravelStorage::disk('backdrop')->delete($backdrop); 59 | } 60 | 61 | /** 62 | * Parse language file. 63 | * 64 | * @return mixed 65 | */ 66 | public function parseLanguage() 67 | { 68 | $alternative = config('app.TRANSLATION'); 69 | $filename = strtolower($alternative) . '.json'; 70 | 71 | // Get english fallback 72 | if( ! LaravelStorage::disk('languages')->exists($filename)) { 73 | $filename = 'en.json'; 74 | } 75 | 76 | return LaravelStorage::disk('languages')->get($filename); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/app/Services/Subpage.php: -------------------------------------------------------------------------------- 1 | itemService = $itemService; 16 | $this->tmdb = $tmdb; 17 | } 18 | 19 | public function item($tmdbId, $mediaType) 20 | { 21 | if($found = $this->itemService->findBy('tmdb_id_strict', $tmdbId, $mediaType)) { 22 | return $found; 23 | } 24 | 25 | $found = $this->tmdb->details($tmdbId, $mediaType); 26 | 27 | if( ! (array) $found) { 28 | return response('Not found', Response::HTTP_NOT_FOUND); 29 | } 30 | 31 | $found->genre_ids = collect($found->genres)->pluck('id')->all(); 32 | 33 | $item = $this->tmdb->createItem($found, $mediaType); 34 | $item['youtube_key'] = $this->itemService->parseYoutubeKey($found, $mediaType); 35 | $item['imdb_id'] = $this->itemService->parseImdbId($found); 36 | 37 | return $item; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/app/Setting.php: -------------------------------------------------------------------------------- 1 | 'boolean', 38 | 'show_genre' => 'boolean', 39 | 'episode_spoiler_protection' => 'boolean', 40 | 'show_watchlist_everywhere' => 'boolean', 41 | 'refresh_automatically' => 'boolean', 42 | 'daily_reminder' => 'boolean', 43 | 'weekly_reminder' => 'boolean', 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /backend/app/User.php: -------------------------------------------------------------------------------- 1 | where('api_key', $key); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/app/helpers.php: -------------------------------------------------------------------------------- 1 | changed->name ?? $file->name; 16 | } 17 | 18 | function mediaType($mediaType) 19 | { 20 | if (strpos($mediaType, 'movie') !== false) { 21 | return 'movie'; 22 | } 23 | 24 | return 'tv'; 25 | } 26 | 27 | function getSlug($title) 28 | { 29 | $slug = Illuminate\Support\Str::slug($title); 30 | 31 | return $slug != '' ? $slug : 'no-slug-available'; 32 | } 33 | 34 | // There is no 'EN' region in TMDb. 35 | function getRegion($translation) 36 | { 37 | return strtolower($translation) == 'en' ? 'us' : $translation; 38 | } 39 | 40 | function logInfo($message, $context = []) 41 | { 42 | if( ! app()->runningUnitTests()) { 43 | info($message, $context); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 32 | 33 | $status = $kernel->handle( 34 | $input = new Symfony\Component\Console\Input\ArgvInput, 35 | new Symfony\Component\Console\Output\ConsoleOutput 36 | ); 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Shutdown The Application 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Once Artisan has finished running. We will fire off the shutdown events 44 | | so that any final work may be done by the application before we shut 45 | | down the process. This is the last thing to happen to the request. 46 | | 47 | */ 48 | 49 | $kernel->terminate($input, $status); 50 | 51 | exit($status); 52 | -------------------------------------------------------------------------------- /backend/bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /backend/bootstrap/autoload.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_KEY'), 36 | 'secret' => env('PUSHER_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | // 40 | ], 41 | ], 42 | 43 | 'redis' => [ 44 | 'driver' => 'redis', 45 | 'connection' => 'default', 46 | ], 47 | 48 | 'log' => [ 49 | 'driver' => 'log', 50 | ], 51 | 52 | 'null' => [ 53 | 'driver' => 'null', 54 | ], 55 | 56 | ], 57 | 58 | ]; 59 | -------------------------------------------------------------------------------- /backend/config/compile.php: -------------------------------------------------------------------------------- 1 | [ 17 | // 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled File Providers 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may list service providers which define a "compiles" function 26 | | that returns additional files that should be compiled, providing an 27 | | easy way to get common files from any packages you are utilizing. 28 | | 29 | */ 30 | 31 | 'providers' => [ 32 | // 33 | ], 34 | 35 | ]; 36 | -------------------------------------------------------------------------------- /backend/config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 1024, 48 | 'threads' => 2, 49 | 'time' => 2, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /backend/config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | ], 21 | 22 | 'ses' => [ 23 | 'key' => env('SES_KEY'), 24 | 'secret' => env('SES_SECRET'), 25 | 'region' => 'us-east-1', 26 | ], 27 | 28 | 'sparkpost' => [ 29 | 'secret' => env('SPARKPOST_SECRET'), 30 | ], 31 | 32 | 'stripe' => [ 33 | 'model' => App\User::class, 34 | 'key' => env('STRIPE_KEY'), 35 | 'secret' => env('STRIPE_SECRET'), 36 | ], 37 | 38 | 'tmdb' => [ 39 | 'key' => env('TMDB_API_KEY'), 40 | 'poster' => 'https://image.tmdb.org/t/p/w185', 41 | 'poster_subpage' => 'https://image.tmdb.org/t/p/w342', 42 | 'backdrop' => 'https://image.tmdb.org/t/p/w1280', 43 | ], 44 | 45 | 'imdb' => [ 46 | 'url' => 'https://www.imdb.com/title/', 47 | ], 48 | 49 | 'fp' => [ 50 | 'host' => env('FP_HOST'), 51 | 'port' => env('FP_PORT'), 52 | ], 53 | 54 | ]; 55 | -------------------------------------------------------------------------------- /backend/config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | realpath(base_path('../client/resources')), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => realpath(storage_path('framework/views')), 32 | 33 | ]; 34 | -------------------------------------------------------------------------------- /backend/database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /backend/database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | define(App\User::class, function(Faker\Generator $faker) { 6 | static $password; 7 | 8 | return [ 9 | 'username' => $faker->name, 10 | 'password' => $password ?: $password = bcrypt('secret'), 11 | 'remember_token' => Illuminate\Support\Str::random(10), 12 | 'api_key' => null, 13 | ]; 14 | }); 15 | 16 | $factory->define(App\Setting::class, function(Faker\Generator $faker) { 17 | return [ 18 | 'show_date' => 1, 19 | 'show_genre' => 1, 20 | 'episode_spoiler_protection' => '', 21 | 'last_fetch_to_file_parser' => null, 22 | ]; 23 | }); 24 | 25 | $factory->define(App\Item::class, function(Faker\Generator $faker) { 26 | return [ 27 | 'poster' => '', 28 | 'rating' => 1, 29 | //'genre' => '', 30 | 'released' => time(), 31 | 'released_timestamp' => now(), 32 | 'last_seen_at' => now(), 33 | 'src' => null, 34 | ]; 35 | }); 36 | 37 | $factory->define(App\Episode::class, function(Faker\Generator $faker) { 38 | return [ 39 | 'name' => $faker->name, 40 | 'season_tmdb_id' => 1, 41 | 'episode_tmdb_id' => 1, 42 | 'src' => null, 43 | ]; 44 | }); 45 | 46 | $factory->state(App\Item::class, 'movie', function() { 47 | return [ 48 | 'media_type' => 'movie', 49 | ]; 50 | }); 51 | 52 | $factory->state(App\Item::class, 'tv', function() { 53 | return [ 54 | 'media_type' => 'tv', 55 | ]; 56 | }); 57 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_08_01_121249_create_items_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 12 | $table->integer('tmdb_id')->unique(); 13 | $table->string('title')->index(); 14 | $table->string('original_title')->index(); 15 | $table->string('poster'); 16 | $table->string('media_type')->default('movie'); 17 | $table->string('genre')->nullable(); 18 | $table->string('rating'); 19 | $table->integer('released'); 20 | $table->integer('created_at'); 21 | }); 22 | } 23 | 24 | public function down() {} 25 | } 26 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_08_05_132344_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 12 | $table->string('username')->unique(); 13 | $table->string('password'); 14 | $table->rememberToken(); 15 | $table->timestamps(); 16 | }); 17 | } 18 | 19 | public function down() {} 20 | } 21 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_10_18_063806_create_settings_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 14 | $table->boolean('show_date')->default(1); 15 | $table->boolean('show_genre')->default(0); 16 | }); 17 | 18 | Setting::create([ 19 | 'show_genre' => 0, 20 | 'show_date' => 1, 21 | ]); 22 | } 23 | 24 | public function down() {} 25 | } 26 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_11_25_090131_create_episodes_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 13 | $table->integer('tmdb_id'); 14 | $table->string('name'); 15 | $table->integer('season_number'); 16 | $table->integer('season_tmdb_id'); 17 | $table->integer('episode_number'); 18 | $table->integer('episode_tmdb_id'); 19 | $table->integer('seen')->default(0); 20 | }); 21 | } 22 | 23 | public function down() {} 24 | } 25 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_11_29_110058_add_created_at_field.php: -------------------------------------------------------------------------------- 1 | integer('created_at')->nullable(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_12_12_074211_add_episode_spoiler_protection_field.php: -------------------------------------------------------------------------------- 1 | boolean('episode_spoiler_protection')->default(1); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_12_15_112332_change_original_title_field.php: -------------------------------------------------------------------------------- 1 | string('original_title')->nullable()->change(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_12_17_144415_add_src_field_for_episodes.php: -------------------------------------------------------------------------------- 1 | text('src')->nullable(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_12_18_195039_add_src_field_for_items.php: -------------------------------------------------------------------------------- 1 | text('src')->nullable(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2016_12_18_195742_create_alternative_titles_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 13 | $table->integer('tmdb_id'); 14 | $table->string('title')->index(); 15 | $table->string('country'); 16 | }); 17 | } 18 | 19 | public function down() {} 20 | } 21 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_01_05_111153_add_last_fetch_to_file_parser_field.php: -------------------------------------------------------------------------------- 1 | timestamp('last_fetch_to_file_parser')->nullable(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_01_25_163036_add_subtitles_field_for_episodes_and_items.php: -------------------------------------------------------------------------------- 1 | text('subtitles')->nullable(); 13 | }); 14 | 15 | Schema::table('items', function (Blueprint $table) { 16 | $table->text('subtitles')->nullable(); 17 | }); 18 | } 19 | 20 | public function down() {} 21 | } 22 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_02_01_122243_change_tmdb_id_field.php: -------------------------------------------------------------------------------- 1 | integer('tmdb_id')->nullable()->change(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_02_02_080109_add_fp_name_field_to_episodes_and_items.php: -------------------------------------------------------------------------------- 1 | string('fp_name')->nullable(); 13 | }); 14 | 15 | Schema::table('episodes', function (Blueprint $table) { 16 | $table->string('fp_name')->nullable(); 17 | }); 18 | } 19 | 20 | public function down() {} 21 | } 22 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_02_22_083139_add_timestamps_to_episodes.php: -------------------------------------------------------------------------------- 1 | dropColumn('created_at'); 13 | }); 14 | 15 | Schema::table('episodes', function (Blueprint $table) { 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() {} 21 | } 22 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_02_22_101320_add_timestamps_and_last_seen_at_to_items.php: -------------------------------------------------------------------------------- 1 | items = Item::get(['id', 'created_at']); 15 | 16 | Schema::table('items', function (Blueprint $table) { 17 | $table->dropColumn('created_at'); 18 | }); 19 | 20 | Schema::table('items', function (Blueprint $table) { 21 | $table->timestamps(); 22 | $table->timestamp('last_seen_at')->nullable(); 23 | }); 24 | 25 | $this->repopulateCreatedAt(); 26 | } 27 | 28 | /** 29 | * We can't change an integer field to a timestamp field. 30 | * So we need to remove the current created_at integer field, 31 | * create the new timestamp fields, and repopulate the old created_at data. 32 | */ 33 | private function repopulateCreatedAt() 34 | { 35 | $this->items->map(function ($item) { 36 | $item->created_at = $item->created_at; 37 | $item->updated_at = $item->created_at; 38 | $item->last_seen_at = $item->created_at; 39 | $item->save(); 40 | }); 41 | } 42 | 43 | public function down() {} 44 | } 45 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_02_23_081946_add_release_dates_to_episodes.php: -------------------------------------------------------------------------------- 1 | integer('release_episode')->nullable(); 13 | $table->integer('release_season')->nullable(); 14 | }); 15 | } 16 | 17 | public function down() {} 18 | } 19 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_03_03_075703_add_subpage_fields_to_items_table.php: -------------------------------------------------------------------------------- 1 | string('backdrop')->nullable(); 13 | $table->string('slug')->nullable(); 14 | $table->string('youtube_key')->nullable(); 15 | $table->string('imdb_id')->nullable(); 16 | $table->text('overview')->nullable(); 17 | $table->string('tmdb_rating')->nullable(); 18 | $table->string('imdb_rating')->nullable(); 19 | }); 20 | } 21 | 22 | public function down() {} 23 | } 24 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_07_24_180742_add_watchlist_field_to_items_tabe.php: -------------------------------------------------------------------------------- 1 | boolean('watchlist')->default(false); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_07_25_095610_add_show_watchlist_everywhere_field.php: -------------------------------------------------------------------------------- 1 | boolean('show_watchlist_everywhere')->default(0); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_08_15_082335_add_show_ratings_field.php: -------------------------------------------------------------------------------- 1 | string('show_ratings')->default('always'); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_12_29_222510_create_genres_table.php: -------------------------------------------------------------------------------- 1 | genreService = app(GenreService::class); 15 | } 16 | 17 | public function up() 18 | { 19 | Schema::create('genres', function (Blueprint $table) { 20 | $table->integer('id'); 21 | $table->string('name'); 22 | }); 23 | 24 | if( ! app()->runningUnitTests()) { 25 | try { 26 | $this->genreService->updateGenreLists(); 27 | } catch (\Exception $e) { 28 | echo 'Can not connect to the TMDb Service on "CreateGenresTable". Error: ' . $e->getMessage(); 29 | echo 'Make sure you set your TMDb API Key in .env'; 30 | 31 | abort(500); 32 | } 33 | } 34 | } 35 | 36 | public function down() {} 37 | } 38 | -------------------------------------------------------------------------------- /backend/database/migrations/2017_12_29_222633_create_genre_item_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 13 | $table->integer('item_id'); 14 | $table->integer('genre_id'); 15 | }); 16 | 17 | // We dont need the genre column more. 18 | Schema::table('items', function (Blueprint $table) { 19 | $table->dropColumn('genre'); 20 | }); 21 | } 22 | 23 | public function down() {} 24 | } 25 | -------------------------------------------------------------------------------- /backend/database/migrations/2018_03_16_235550_add_refreshed_at_field.php: -------------------------------------------------------------------------------- 1 | timestamp('refreshed_at')->nullable(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2018_04_29_162253_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('queue')->index(); 19 | $table->longText('payload'); 20 | $table->unsignedTinyInteger('attempts'); 21 | $table->unsignedInteger('reserved_at')->nullable(); 22 | $table->unsignedInteger('available_at'); 23 | $table->unsignedInteger('created_at'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('jobs'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/database/migrations/2018_04_29_164313_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/database/migrations/2018_11_10_180540_add_refresh_automatically_field.php: -------------------------------------------------------------------------------- 1 | boolean('refresh_automatically')->default(0); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2018_11_10_180653_add_reminders_send_to_field.php: -------------------------------------------------------------------------------- 1 | string('reminders_send_to')->nullable(); 13 | }); 14 | } 15 | 16 | public function down() {} 17 | } 18 | -------------------------------------------------------------------------------- /backend/database/migrations/2018_11_10_180828_add_daily_and_weekly_fields.php: -------------------------------------------------------------------------------- 1 | boolean('daily_reminder')->default(0); 13 | $table->boolean('weekly_reminder')->default(0); 14 | }); 15 | } 16 | 17 | public function down() {} 18 | } 19 | -------------------------------------------------------------------------------- /backend/database/migrations/2018_12_28_230635_add_homepage_field_to_items_table.php: -------------------------------------------------------------------------------- 1 | string('homepage')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('items', function (Blueprint $table) { 29 | // 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_12_23_122213_add_api_key_to_users.php: -------------------------------------------------------------------------------- 1 | string('api_key')->nullable()->index(); 18 | }); 19 | } 20 | 21 | public function down(){} 22 | } 23 | -------------------------------------------------------------------------------- /backend/database/migrations/2019_12_24_104600_add_released_timestamp_to_items.php: -------------------------------------------------------------------------------- 1 | timestamp('released_timestamp')->nullable(); 20 | }); 21 | 22 | Item::query()->each(function (Item $item) { 23 | $item->update([ 24 | 'released_timestamp' => Carbon::parse($item->released), 25 | ]); 26 | }); 27 | } 28 | 29 | public function down(){} 30 | } 31 | -------------------------------------------------------------------------------- /backend/database/migrations/2020_01_09_210708_change_released_timestamp_to_datetime.php: -------------------------------------------------------------------------------- 1 | dateTime('released_timestamp')->change(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/database/migrations/2020_01_27_105656_drop_tmdb_id_unique_index.php: -------------------------------------------------------------------------------- 1 | dropUnique(['tmdb_id']); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/database/seeds/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/database/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /backend/routes/api.php: -------------------------------------------------------------------------------- 1 | 'auth.basic'], function() { 4 | Route::patch('/update-files', 'FileParserController@receive'); 5 | Route::post('/import', 'ExportImportController@import'); 6 | Route::get('/export', 'ExportImportController@export'); 7 | }); 8 | 9 | Route::get('/last-fetched', 'FileParserController@lastFetched'); 10 | -------------------------------------------------------------------------------- /backend/routes/console.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /backend/storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /backend/storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | -------------------------------------------------------------------------------- /backend/storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/tests/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Schema::hasTable('users')); 16 | $this->assertTrue(Schema::hasTable('items')); 17 | $this->assertTrue(Schema::hasTable('settings')); 18 | $this->assertTrue(Schema::hasTable('episodes')); 19 | $this->assertTrue(Schema::hasColumn('settings', 'episode_spoiler_protection')); 20 | $this->assertTrue(Schema::hasColumn('episodes', 'src')); 21 | $this->assertTrue(Schema::hasColumn('items', 'src')); 22 | $this->assertTrue(Schema::hasTable('alternative_titles')); 23 | $this->assertTrue(Schema::hasTable('alternative_titles')); 24 | $this->assertTrue(Schema::hasColumn('settings', 'last_fetch_to_file_parser')); 25 | $this->assertTrue(Schema::hasColumn('items', 'subtitles')); 26 | $this->assertTrue(Schema::hasColumn('episodes', 'subtitles')); 27 | $this->assertTrue(Schema::hasColumn('items', 'fp_name')); 28 | $this->assertTrue(Schema::hasColumn('episodes', 'fp_name')); 29 | $this->assertTrue(Schema::hasColumn('episodes', 'created_at')); 30 | $this->assertTrue(Schema::hasColumn('episodes', 'updated_at')); 31 | $this->assertTrue(Schema::hasColumn('items', 'last_seen_at')); 32 | $this->assertTrue(Schema::hasColumn('episodes', 'release_episode')); 33 | $this->assertTrue(Schema::hasColumn('episodes', 'release_season')); 34 | } 35 | 36 | /** @test */ 37 | public function it_can_call_homepage_successfully() 38 | { 39 | $response = $this->call('GET', '/'); 40 | 41 | $response->assertSuccessful(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 20 | 21 | Hash::setRounds(4); 22 | 23 | return $app; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/tests/Services/Api/ApiTestInterface.php: -------------------------------------------------------------------------------- 1 | apiTest = app(ApiTest::class); 20 | 21 | $this->apiTest->apiClass = FakeApi::class; 22 | 23 | $this->apiTest->setUp(); 24 | } 25 | 26 | /** @test */ 27 | public function it_should_abort_the_request() 28 | { 29 | $this->apiTest->it_should_abort_the_request('fake/abort.json'); 30 | } 31 | 32 | /** @test */ 33 | public function it_should_create_a_new_movie() 34 | { 35 | $this->apiTest->it_should_create_a_new_movie('fake/movie.json'); 36 | } 37 | 38 | /** @test */ 39 | public function it_should_not_create_a_new_movie_if_it_exists() 40 | { 41 | $this->apiTest->it_should_not_create_a_new_movie_if_it_exists('fake/movie.json'); 42 | } 43 | 44 | /** @test */ 45 | public function it_should_create_a_new_tv_show() 46 | { 47 | $this->apiTest->it_should_create_a_new_tv_show('fake/tv.json'); 48 | } 49 | 50 | /** @test */ 51 | public function it_should_not_create_a_new_tv_show_if_it_exists() 52 | { 53 | $this->apiTest->it_should_not_create_a_new_tv_show_if_it_exists('fake/tv.json'); 54 | } 55 | 56 | /** @test */ 57 | public function it_should_rate_a_movie() 58 | { 59 | $this->apiTest->it_should_rate_a_movie('fake/movie_rating.json', 2); 60 | } 61 | 62 | /** @test */ 63 | public function it_should_rate_a_tv_show() 64 | { 65 | $this->apiTest->it_should_rate_a_tv_show('fake/tv_rating.json', 3); 66 | } 67 | 68 | /** @test */ 69 | public function it_should_mark_an_episode_as_seen() 70 | { 71 | $this->apiTest->it_should_mark_an_episode_as_seen('fake/episode_seen.json'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /backend/tests/Services/Api/PlexApiTest.php: -------------------------------------------------------------------------------- 1 | apiTest = app(ApiTest::class); 20 | 21 | $this->apiTest->apiClass = Plex::class; 22 | 23 | $this->apiTest->setUp(); 24 | } 25 | 26 | /** @test */ 27 | public function it_should_abort_the_request() 28 | { 29 | $this->apiTest->it_should_abort_the_request('plex/abort.json'); 30 | } 31 | 32 | /** @test */ 33 | public function it_should_create_a_new_movie() 34 | { 35 | $this->apiTest->it_should_create_a_new_movie('plex/movie.json'); 36 | } 37 | 38 | /** @test */ 39 | public function it_should_not_create_a_new_movie_if_it_exists() 40 | { 41 | $this->apiTest->it_should_not_create_a_new_movie_if_it_exists('plex/movie.json'); 42 | } 43 | 44 | /** @test */ 45 | public function it_should_create_a_new_tv_show() 46 | { 47 | $this->apiTest->it_should_create_a_new_tv_show('plex/tv.json'); 48 | } 49 | 50 | /** @test */ 51 | public function it_should_not_create_a_new_tv_show_if_it_exists() 52 | { 53 | $this->apiTest->it_should_not_create_a_new_tv_show_if_it_exists('plex/tv.json'); 54 | } 55 | 56 | /** @test */ 57 | public function it_should_rate_a_movie() 58 | { 59 | $this->apiTest->it_should_rate_a_movie('plex/movie_rating.json', 2); 60 | } 61 | 62 | /** @test */ 63 | public function it_should_rate_a_tv_show() 64 | { 65 | $this->apiTest->it_should_rate_a_tv_show('plex/tv_rating.json', 3); 66 | } 67 | 68 | /** @test */ 69 | public function it_should_mark_an_episode_as_seen() 70 | { 71 | $this->apiTest->it_should_mark_an_episode_as_seen('plex/episode_seen.json'); 72 | } 73 | 74 | /** @test */ 75 | public function it_should_update_last_seen_at_of_a_show() 76 | { 77 | $this->apiTest->it_should_update_last_seen_at('plex/episode_seen.json'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /backend/tests/Services/GenreServiceTest.php: -------------------------------------------------------------------------------- 1 | createGuzzleMock( 26 | $this->tmdbFixtures('movie/genres'), 27 | $this->tmdbFixtures('tv/genres') 28 | ); 29 | 30 | $service = app(GenreService::class); 31 | 32 | $genresBeforeUpdate = Genre::all(); 33 | 34 | $service->updateGenreLists(); 35 | 36 | $genresAfterUpdate = Genre::all(); 37 | 38 | $this->assertCount(0, $genresBeforeUpdate); 39 | $this->assertCount(27, $genresAfterUpdate); 40 | } 41 | 42 | /** @test */ 43 | public function it_should_sync_genres_for_an_item() 44 | { 45 | $genreIds = [28, 12, 16]; 46 | 47 | $this->createGuzzleMock( 48 | $this->tmdbFixtures('movie/genres'), 49 | $this->tmdbFixtures('tv/genres') 50 | ); 51 | 52 | $item = $this->createMovie(); 53 | 54 | $service = app(GenreService::class); 55 | $service->updateGenreLists(); 56 | 57 | $itemBeforeUpdate = Item::first(); 58 | 59 | $service->sync($item, $genreIds); 60 | 61 | $itemAfterUpdate = Item::first(); 62 | 63 | $this->assertCount(0, $itemBeforeUpdate->genre); 64 | $this->assertCount(count($genreIds), $itemAfterUpdate->genre); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/tests/Services/IMDBTest.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/../fixtures/imdb/with-rating.html']); 17 | 18 | $imdbService = app(IMDB::class); 19 | 20 | $rating = $imdbService->parseRating(); 21 | 22 | $this->assertEquals('7,0', $rating); 23 | } 24 | 25 | /** @test */ 26 | public function it_should_return_null_if_no_rating_was_found() 27 | { 28 | config(['services.imdb.url' => __DIR__ . '/../fixtures/imdb/without-rating.html']); 29 | 30 | $imdbService = app(IMDB::class); 31 | 32 | $rating = $imdbService->parseRating(); 33 | 34 | $this->assertNull($rating); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/tests/TestCase.php: -------------------------------------------------------------------------------- 1 | create($custom); 15 | } 16 | 17 | public function createSetting() 18 | { 19 | return factory(Setting::class)->create(); 20 | } 21 | 22 | public function createMovie($custom = []) 23 | { 24 | $data = [ 25 | 'title' => 'Warcraft: The Beginning', 26 | 'original_title' => 'Warcraft', 27 | 'tmdb_id' => 68735, 28 | 'media_type' => 'movie', 29 | ]; 30 | 31 | return factory(Item::class)->create(array_merge($data, $custom)); 32 | } 33 | 34 | public function createTv($custom = [], $withEpisodes = true) 35 | { 36 | $data = [ 37 | 'title' => 'Game of Thrones', 38 | 'original_title' => 'Game of Thrones', 39 | 'tmdb_id' => 1399, 40 | 'media_type' => 'tv', 41 | ]; 42 | 43 | factory(Item::class)->create(array_merge($data, $custom)); 44 | 45 | if($withEpisodes) { 46 | foreach([1, 2] as $season) { 47 | foreach([1, 2] as $episode) { 48 | factory(Episode::class)->create([ 49 | 'tmdb_id' => 1399, 50 | 'season_number' => $season, 51 | 'episode_number' => $episode, 52 | ]); 53 | } 54 | } 55 | } 56 | } 57 | 58 | public function getMovie($custom = []) 59 | { 60 | $data = [ 61 | 'title' => 'Warcraft', 62 | 'tmdb_id' => 68735, 63 | ]; 64 | 65 | return factory(Item::class)->states('movie')->make(array_merge($data, $custom)); 66 | } 67 | 68 | public function getTv($custom = []) 69 | { 70 | $data = [ 71 | 'title' => 'Game of Thrones', 72 | 'tmdb_id' => 1399, 73 | ]; 74 | 75 | return factory(Item::class)->states('tv')->make(array_merge($data, $custom)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/tests/Traits/Fixtures.php: -------------------------------------------------------------------------------- 1 | toArray(); 25 | } 26 | 27 | protected function apiFixtures($path) 28 | { 29 | return collect(json_decode(file_get_contents(__DIR__ . '/../fixtures/api/' . $path), true))->toArray(); 30 | } 31 | 32 | protected function getMovieSrc() 33 | { 34 | return '/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv'; 35 | } 36 | 37 | protected function getTvSrc($episode) 38 | { 39 | return '/tv/Game of Thrones/S' . $episode->season_number . '/' . $episode->episode_number . '.mkv'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/tests/Traits/Mocks.php: -------------------------------------------------------------------------------- 1 | [40]], $fixture); 24 | } 25 | 26 | $mock = new MockHandler($responses); 27 | 28 | $handler = HandlerStack::create($mock); 29 | $this->app->instance(Client::class, new Client(['handler' => $handler])); 30 | } 31 | 32 | public function createStorageDownloadsMock() 33 | { 34 | $mock = $this->mock(Storage::class); 35 | $mock->shouldReceive('downloadImages')->andReturn(null); 36 | } 37 | 38 | public function createRefreshAllMock() 39 | { 40 | $mock = $this->mock(ItemService::class); 41 | $mock->shouldReceive('refreshAll')->andReturn(null); 42 | } 43 | 44 | public function createTmdbEpisodeMock() 45 | { 46 | // Mock this to avoid unknown requests to TMDb (get seasons and then get episodes for each season) 47 | $mock = $this->mock(TMDB::class); 48 | $mock->shouldReceive('tvEpisodes')->andReturn(json_decode($this->tmdbFixtures('tv/episodes'))); 49 | } 50 | 51 | private function createImdbRatingMock() 52 | { 53 | $mock = $this->mock(IMDB::class); 54 | $mock->shouldReceive('parseRating')->andReturn(json_decode($this->imdbFixtures('rating.txt'))); 55 | } 56 | 57 | public function mock($class, $mock = null) 58 | { 59 | $mock = Mockery::mock(app($class))->makePartial(); 60 | 61 | $this->app->instance($class, $mock); 62 | 63 | return $mock; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /backend/tests/fixtures/FakeApi.php: -------------------------------------------------------------------------------- 1 | data['data']['abort']; 16 | } 17 | 18 | /** 19 | * @inheritDoc 20 | */ 21 | protected function getType() 22 | { 23 | return $this->data['data']['type']; 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | protected function getTitle() 30 | { 31 | return $this->data['data']['title']; 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | protected function getRating() 38 | { 39 | return $this->data['data']['rating']; 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | protected function shouldRateItem() 46 | { 47 | return $this->data['data']['rate']; 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | protected function shouldEpisodeMarkedAsSeen() 54 | { 55 | return $this->data['data']['seen']; 56 | } 57 | 58 | /** 59 | * @inheritDoc 60 | */ 61 | protected function getEpisodeNumber() 62 | { 63 | return $this->data['data']['episode']; 64 | } 65 | 66 | /** 67 | * @inheritDoc 68 | */ 69 | protected function getSeasonNumber() 70 | { 71 | return $this->data['data']['season']; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/fake/abort.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "abort": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/fake/episode_seen.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "abort": false, 4 | "type": "tv", 5 | "title": "Game of Thrones", 6 | "rate": false, 7 | "seen": true, 8 | "rating": null, 9 | "episode": 2, 10 | "season": 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/fake/movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "abort": false, 4 | "type": "movie", 5 | "title": "Warcraft", 6 | "rate": false, 7 | "seen": false, 8 | "rating": null, 9 | "episode": null, 10 | "season": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/fake/movie_rating.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "abort": false, 4 | "type": "movie", 5 | "title": "Warcraft", 6 | "rate": true, 7 | "seen": false, 8 | "rating": 2, 9 | "episode": null, 10 | "season": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/fake/tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "abort": false, 4 | "type": "tv", 5 | "title": "Game of Thrones", 6 | "rate": false, 7 | "seen": false, 8 | "rating": null, 9 | "episode": null, 10 | "season": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/fake/tv_rating.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "abort": false, 4 | "type": "tv", 5 | "title": "Game of Thrones", 6 | "rate": true, 7 | "seen": false, 8 | "rating": 3, 9 | "episode": null, 10 | "season": null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/plex/abort.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "media.play", 3 | "user": true, 4 | "owner": true, 5 | "Account": {}, 6 | "Server": {}, 7 | "Player": {}, 8 | "Metadata": { 9 | "librarySectionType": "", 10 | "ratingKey": "", 11 | "key": "", 12 | "parentRatingKey": "", 13 | "grandparentRatingKey": "", 14 | "guid": "", 15 | "parentGuid": "", 16 | "grandparentGuid": "", 17 | "type": "podcast", 18 | "title": "a title here", 19 | "titleSort": "", 20 | "grandparentKey": "", 21 | "parentKey": "", 22 | "grandparentTitle": "Game of Thrones", 23 | "parentTitle": "Season 1", 24 | "contentRating": "", 25 | "summary": "", 26 | "index": 2, 27 | "parentIndex": 1, 28 | "rating": null, 29 | "userRating": null, 30 | "viewCount": null, 31 | "lastViewedAt": null, 32 | "lastRatedAt": null, 33 | "year": null, 34 | "thumb": "", 35 | "art": "", 36 | "parentThumb": "", 37 | "grandparentThumb": "", 38 | "grandparentArt": "", 39 | "grandparentTheme": "", 40 | "originallyAvailableAt": "", 41 | "addedAt": null, 42 | "updatedAt": null, 43 | "Writer": [] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/plex/episode_seen.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "media.scrobble", 3 | "user": true, 4 | "owner": true, 5 | "Account": {}, 6 | "Server": {}, 7 | "Player": {}, 8 | "Metadata": { 9 | "librarySectionType": "", 10 | "ratingKey": "", 11 | "key": "", 12 | "parentRatingKey": "", 13 | "grandparentRatingKey": "", 14 | "guid": "", 15 | "parentGuid": "", 16 | "grandparentGuid": "", 17 | "type": "episode", 18 | "title": "", 19 | "titleSort": "", 20 | "grandparentKey": "", 21 | "parentKey": "", 22 | "grandparentTitle": "Game of Thrones", 23 | "parentTitle": "", 24 | "contentRating": "", 25 | "summary": "", 26 | "index": 2, 27 | "parentIndex": 1, 28 | "rating": null, 29 | "userRating": null, 30 | "viewCount": null, 31 | "lastViewedAt": null, 32 | "lastRatedAt": null, 33 | "year": null, 34 | "thumb": "", 35 | "art": "", 36 | "parentThumb": "", 37 | "grandparentThumb": "", 38 | "grandparentArt": "", 39 | "grandparentTheme": "", 40 | "originallyAvailableAt": "", 41 | "addedAt": null, 42 | "updatedAt": null, 43 | "Writer": [] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/plex/movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "media.play", 3 | "user": true, 4 | "owner": true, 5 | "Account": {}, 6 | "Server": {}, 7 | "Player": {}, 8 | "Metadata": { 9 | "librarySectionType": "", 10 | "ratingKey": "", 11 | "key": "", 12 | "guid": "", 13 | "studio": "", 14 | "type": "movie", 15 | "title": "Warcraft", 16 | "contentRating": "", 17 | "summary": "", 18 | "rating": null, 19 | "audienceRating": null, 20 | "userRating": null, 21 | "viewCount": null, 22 | "lastViewedAt": null, 23 | "lastRatedAt": null, 24 | "year": null, 25 | "tagline": "", 26 | "thumb": "", 27 | "art": "", 28 | "duration": null, 29 | "originallyAvailableAt": "", 30 | "addedAt": null, 31 | "updatedAt": null, 32 | "audienceRatingImage": "", 33 | "primaryExtraKey": "", 34 | "ratingImage": "", 35 | "Genre": [], 36 | "Director": [], 37 | "Writer": [], 38 | "Producer": [], 39 | "Country": [], 40 | "Role": [], 41 | "Similar": [] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/plex/movie_rating.json: -------------------------------------------------------------------------------- 1 | { 2 | "rating": "", 3 | "event": "media.rate", 4 | "user": true, 5 | "owner": true, 6 | "Account": {}, 7 | "Server": {}, 8 | "Player": {}, 9 | "Metadata": { 10 | "librarySectionType": "", 11 | "ratingKey": "", 12 | "key": "", 13 | "guid": "", 14 | "studio": "", 15 | "type": "movie", 16 | "title": "Warcraft", 17 | "contentRating": "", 18 | "summary": "", 19 | "rating": null, 20 | "audienceRating": null, 21 | "userRating": 5, 22 | "viewCount": null, 23 | "lastViewedAt": null, 24 | "lastRatedAt": null, 25 | "year": null, 26 | "tagline": "", 27 | "thumb": "", 28 | "art": "", 29 | "duration": null, 30 | "originallyAvailableAt": "", 31 | "addedAt": null, 32 | "updatedAt": null, 33 | "audienceRatingImage": "", 34 | "primaryExtraKey": "", 35 | "ratingImage": "", 36 | "Genre": [], 37 | "Director": [], 38 | "Writer": [], 39 | "Producer": [], 40 | "Country": [], 41 | "Role": [], 42 | "Similar": [] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/plex/tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "media.play", 3 | "user": true, 4 | "owner": true, 5 | "Account": {}, 6 | "Server": {}, 7 | "Player": {}, 8 | "Metadata": { 9 | "librarySectionType": "", 10 | "ratingKey": "", 11 | "key": "", 12 | "parentRatingKey": "", 13 | "grandparentRatingKey": "", 14 | "guid": "", 15 | "parentGuid": "", 16 | "grandparentGuid": "", 17 | "type": "episode", 18 | "title": "", 19 | "titleSort": "", 20 | "grandparentKey": "", 21 | "parentKey": "", 22 | "grandparentTitle": "Game of Thrones", 23 | "parentTitle": "", 24 | "contentRating": "", 25 | "summary": "", 26 | "index": 2, 27 | "parentIndex": 1, 28 | "rating": null, 29 | "userRating": null, 30 | "viewCount": null, 31 | "lastViewedAt": null, 32 | "lastRatedAt": null, 33 | "year": null, 34 | "thumb": "", 35 | "art": "", 36 | "parentThumb": "", 37 | "grandparentThumb": "", 38 | "grandparentArt": "", 39 | "grandparentTheme": "", 40 | "originallyAvailableAt": "", 41 | "addedAt": null, 42 | "updatedAt": null, 43 | "Writer": [] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/tests/fixtures/api/plex/tv_rating.json: -------------------------------------------------------------------------------- 1 | { 2 | "rating": "", 3 | "event": "media.rate", 4 | "user": true, 5 | "owner": true, 6 | "Account": {}, 7 | "Server": {}, 8 | "Player": {}, 9 | "Metadata": { 10 | "librarySectionType": "", 11 | "ratingKey": "", 12 | "key": "", 13 | "guid": "", 14 | "studio": "", 15 | "type": "show", 16 | "title": "Game of Thrones", 17 | "contentRating": "", 18 | "summary": "", 19 | "index": null, 20 | "rating": null, 21 | "userRating": 1, 22 | "viewCount": null, 23 | "lastViewedAt": null, 24 | "lastRatedAt": null, 25 | "year": null, 26 | "thumb": "", 27 | "art": "", 28 | "banner": "", 29 | "theme": "", 30 | "duration": null, 31 | "originallyAvailableAt": "", 32 | "leafCount": null, 33 | "viewedLeafCount": null, 34 | "childCount": null, 35 | "addedAt": null, 36 | "updatedAt": null, 37 | "Genre": [], 38 | "Role": [], 39 | "Similar": [], 40 | "Location": [] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/tests/fixtures/flox/movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "tmdb_id": 68735, 3 | "title": "Warcraft: The Beginning", 4 | "original_title": "Warcraft", 5 | "poster": "\/jVT1MYp58SSzXc6BKetkGxVS3Ze.jpg", 6 | "media_type": "movie", 7 | "released": 1464185524, 8 | "genre": "Adventure, Fantasy, Action", 9 | "genre_ids": [1,2,3], 10 | "episodes": [] 11 | } 12 | -------------------------------------------------------------------------------- /backend/tests/fixtures/flox/tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "tmdb_id": 1399, 3 | "title": "Game of Thrones", 4 | "original_title": "Game of Thrones", 5 | "poster": "\/jIhL6mlT7AblhbHJgEoiBIOUVl1.jpg", 6 | "media_type": "tv", 7 | "released": 1303051450, 8 | "genre": "Sci-Fi & Fantasy, Action & Adventure, Drama", 9 | "genre_ids": [1,2,3], 10 | "episodes": [] 11 | } 12 | -------------------------------------------------------------------------------- /backend/tests/fixtures/flox/wrong-file.txt: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/added.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "warcraft", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "added", 13 | "subtitles": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.srt" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/added_not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "NOT EXISTS MOVIE", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "added", 13 | "subtitles": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.srt" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/removed.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "warcraft", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "removed", 13 | "subtitles": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.srt" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/unknown.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "warcraft", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "unknown", 13 | "subtitles": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.srt" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "warcraft", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "updated", 13 | "subtitles": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.srt", 14 | "changed": { 15 | "name": "NEW NAME", 16 | "src": "NEW SRC", 17 | "subtitles": "NEW SUB" 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/updated_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "NOT EXISTS MOVIE", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "updated", 13 | "subtitles": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.srt", 14 | "changed": { 15 | "name": "warcraft", 16 | "src": "NEW SRC", 17 | "subtitles": "NEW SUB" 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/updated_is_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "warcraft", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "updated", 13 | "subtitles": "SUB", 14 | "changed": {} 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/movie/updated_not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "movies": [ 3 | { 4 | "name": "NOT EXISTS MOVIE", 5 | "extension": "mkv", 6 | "filename": "Warcraft.2016.720p.WEB-DL", 7 | "src": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.mkv", 8 | "year": 2016, 9 | "tags": [ 10 | "720p" 11 | ], 12 | "status": "updated", 13 | "subtitles": "/movies/Warcraft.2016.720p.WEB-DL/Warcraft.2016.720p.WEB-DL.srt", 14 | "changed": { 15 | "name": "NEW NAME", 16 | "src": "NEW SRC", 17 | "subtitles": "NEW SUB" 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/added.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "Game of Thrones", 5 | "season_number": 2, 6 | "episode_number": 1, 7 | "status": "added", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": "src", 13 | "src": "/tv/Game of Thrones/S1/1.mkv" 14 | }, 15 | { 16 | "name": "Game of Thrones", 17 | "season_number": 2, 18 | "episode_number": 2, 19 | "tags": [], 20 | "status": "added", 21 | "extension": "mkv", 22 | "year": null, 23 | "filename": "2", 24 | "subtitles": "src", 25 | "src": "/tv/Game of Thrones/S1/1.mkv" 26 | }, 27 | { 28 | "name": "Game of Thrones", 29 | "season_number": 1, 30 | "episode_number": 1, 31 | "extension": "mkv", 32 | "status": "added", 33 | "filename": "1", 34 | "tags": [], 35 | "subtitles": "src", 36 | "year": null, 37 | "src": "/tv/Game of Thrones/s1/1.mkv" 38 | }, 39 | { 40 | "name": "Game of Thrones", 41 | "season_number": 1, 42 | "tags": [], 43 | "episode_number": 2, 44 | "status": "added", 45 | "extension": "mp4", 46 | "filename": "2", 47 | "year": null, 48 | "subtitles": "src", 49 | "src": "/tv/Game of Thrones/s1/1.mkv" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/added_not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "NOT EXISTS TV", 5 | "season_number": 2, 6 | "episode_number": 3, 7 | "status": "added", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": "src", 13 | "src": "/tv/Game of Thrones/S1/1.mkv" 14 | }, 15 | { 16 | "name": "NOT EXISTS TV", 17 | "season_number": 2, 18 | "episode_number": 4, 19 | "tags": [], 20 | "status": "added", 21 | "extension": "mkv", 22 | "year": null, 23 | "filename": "2", 24 | "subtitles": "src", 25 | "src": "/tv/Game of Thrones/S1/1.mkv" 26 | }, 27 | { 28 | "name": "NOT EXISTS TV", 29 | "season_number": 1, 30 | "episode_number": 3, 31 | "extension": "mkv", 32 | "status": "added", 33 | "filename": "1", 34 | "tags": [], 35 | "subtitles": "src", 36 | "year": null, 37 | "src": "/tv/Game of Thrones/s1/1.mkv" 38 | }, 39 | { 40 | "name": "NOT EXISTS TV", 41 | "season_number": 1, 42 | "tags": [], 43 | "episode_number": 4, 44 | "status": "added", 45 | "extension": "mp4", 46 | "filename": "2", 47 | "year": null, 48 | "subtitles": "src", 49 | "src": "/tv/Game of Thrones/s1/1.mkv" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/removed.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "Game of Thrones", 5 | "season_number": 2, 6 | "episode_number": 1, 7 | "status": "removed", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": null, 13 | "src": "/tv/Game of Thrones/S1/1.mkv" 14 | }, 15 | { 16 | "name": "Game of Thrones", 17 | "season_number": 2, 18 | "episode_number": 2, 19 | "tags": [], 20 | "status": "removed", 21 | "extension": "mkv", 22 | "year": null, 23 | "filename": "2", 24 | "subtitles": null, 25 | "src": "/tv/Game of Thrones/S1/1.mkv" 26 | }, 27 | { 28 | "name": "Game of Thrones", 29 | "season_number": 1, 30 | "episode_number": 1, 31 | "extension": "mkv", 32 | "status": "removed", 33 | "filename": "1", 34 | "tags": [], 35 | "subtitles": null, 36 | "year": null, 37 | "src": "/tv/Game of Thrones/s1/1.mkv" 38 | }, 39 | { 40 | "name": "Game of Thrones", 41 | "season_number": 1, 42 | "tags": [], 43 | "episode_number": 2, 44 | "status": "removed", 45 | "extension": "mp4", 46 | "filename": "2", 47 | "year": null, 48 | "subtitles": null, 49 | "src": "/tv/Game of Thrones/s1/1.mkv" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/unknown.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "Game of Thrones", 5 | "season_number": 1, 6 | "tags": [], 7 | "episode_number": 2, 8 | "status": "unknown", 9 | "extension": "mp4", 10 | "filename": "2", 11 | "year": null, 12 | "subtitles": "src", 13 | "src": "/tv/Game of Thrones/s1/1.mkv" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "Game of Thrones", 5 | "season_number": 2, 6 | "episode_number": 1, 7 | "status": "updated", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": null, 13 | "src": "/tv/Game of Thrones/S1/1.mkv", 14 | "changed": { 15 | "src": "NEW SRC", 16 | "subtitles": "NEW SUB" 17 | } 18 | }, 19 | { 20 | "name": "Game of Thrones", 21 | "season_number": 2, 22 | "episode_number": 2, 23 | "tags": [], 24 | "status": "updated", 25 | "extension": "mkv", 26 | "year": null, 27 | "filename": "2", 28 | "subtitles": null, 29 | "src": "/tv/Game of Thrones/S1/1.mkv", 30 | "changed": { 31 | "src": "NEW SRC", 32 | "subtitles": "NEW SUB" 33 | } 34 | }, 35 | { 36 | "name": "Game of Thrones", 37 | "season_number": 1, 38 | "episode_number": 2, 39 | "tags": [], 40 | "status": "updated", 41 | "extension": "mkv", 42 | "year": null, 43 | "filename": "2", 44 | "subtitles": null, 45 | "src": "/tv/Game of Thrones/S1/1.mkv", 46 | "changed": { 47 | "src": "NEW SRC", 48 | "subtitles": "NEW SUB" 49 | } 50 | }, 51 | { 52 | "name": "Game of Thrones", 53 | "season_number": 1, 54 | "episode_number": 1, 55 | "tags": [], 56 | "status": "updated", 57 | "extension": "mkv", 58 | "year": null, 59 | "filename": "2", 60 | "subtitles": null, 61 | "src": "/tv/Game of Thrones/S1/1.mkv", 62 | "changed": { 63 | "src": "NEW SRC", 64 | "subtitles": "NEW SUB" 65 | } 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/updated_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "NOT EXISTS TV", 5 | "season_number": 1, 6 | "episode_number": 1, 7 | "status": "updated", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": "src", 13 | "src": "/tv/Game of Thrones/S1/1.mkv", 14 | "changed": { 15 | "name": "Game of Thrones", 16 | "src": "NEW SRC", 17 | "subtitles": "NEW SUB" 18 | } 19 | }, 20 | { 21 | "name": "NOT EXISTS TV", 22 | "season_number": 1, 23 | "episode_number": 2, 24 | "tags": [], 25 | "status": "updated", 26 | "extension": "mkv", 27 | "year": null, 28 | "filename": "2", 29 | "subtitles": "src", 30 | "src": "/tv/Game of Thrones/S1/1.mkv", 31 | "changed": { 32 | "name": "Game of Thrones", 33 | "src": "NEW SRC", 34 | "subtitles": "NEW SUB" 35 | } 36 | }, 37 | { 38 | "name": "NOT EXISTS TV", 39 | "season_number": 2, 40 | "episode_number": 1, 41 | "extension": "mkv", 42 | "status": "updated", 43 | "filename": "1", 44 | "tags": [], 45 | "subtitles": "src", 46 | "year": null, 47 | "src": "/tv/Game of Thrones/s1/1.mkv", 48 | "changed": { 49 | "name": "Game of Thrones", 50 | "src": "NEW SRC", 51 | "subtitles": "NEW SUB" 52 | } 53 | }, 54 | { 55 | "name": "NOT EXISTS TV", 56 | "season_number": 2, 57 | "tags": [], 58 | "episode_number": 2, 59 | "status": "updated", 60 | "extension": "mp4", 61 | "filename": "2", 62 | "year": null, 63 | "subtitles": "src", 64 | "src": "/tv/Game of Thrones/s1/1.mkv", 65 | "changed": { 66 | "name": "Game of Thrones", 67 | "src": "NEW SRC", 68 | "subtitles": "NEW SUB" 69 | } 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/updated_is_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "Game of Thrones", 5 | "season_number": 2, 6 | "episode_number": 1, 7 | "status": "updated", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": null, 13 | "src": "/tv/Game of Thrones/S2/1.mkv", 14 | "changed": {} 15 | }, 16 | { 17 | "name": "Game of Thrones", 18 | "season_number": 2, 19 | "episode_number": 2, 20 | "tags": [], 21 | "status": "updated", 22 | "extension": "mkv", 23 | "year": null, 24 | "filename": "2", 25 | "subtitles": null, 26 | "src": "/tv/Game of Thrones/S2/2.mkv", 27 | "changed": {} 28 | }, 29 | { 30 | "name": "Game of Thrones", 31 | "season_number": 1, 32 | "episode_number": 2, 33 | "tags": [], 34 | "status": "updated", 35 | "extension": "mkv", 36 | "year": null, 37 | "filename": "2", 38 | "subtitles": null, 39 | "src": "/tv/Game of Thrones/S1/2.mkv", 40 | "changed": {} 41 | }, 42 | { 43 | "name": "Game of Thrones", 44 | "season_number": 1, 45 | "episode_number": 1, 46 | "tags": [], 47 | "status": "updated", 48 | "extension": "mkv", 49 | "year": null, 50 | "filename": "2", 51 | "subtitles": null, 52 | "src": "/tv/Game of Thrones/S1/1.mkv", 53 | "changed": {} 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/updated_not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "NOT EXISTS TV", 5 | "season_number": 2, 6 | "episode_number": 3, 7 | "status": "updated", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": "src", 13 | "src": "/tv/Game of Thrones/S1/1.mkv", 14 | "changed": { 15 | "name": "NEW NAME", 16 | "src": "NEW SRC", 17 | "subtitles": "NEW SUB" 18 | } 19 | }, 20 | { 21 | "name": "NOT EXISTS TV", 22 | "season_number": 2, 23 | "episode_number": 4, 24 | "tags": [], 25 | "status": "updated", 26 | "extension": "mkv", 27 | "year": null, 28 | "filename": "2", 29 | "subtitles": "src", 30 | "src": "/tv/Game of Thrones/S1/1.mkv", 31 | "changed": { 32 | "name": "NEW NAME", 33 | "src": "NEW SRC", 34 | "subtitles": "NEW SUB" 35 | } 36 | }, 37 | { 38 | "name": "NOT EXISTS TV", 39 | "season_number": 1, 40 | "episode_number": 3, 41 | "extension": "mkv", 42 | "status": "updated", 43 | "filename": "1", 44 | "tags": [], 45 | "subtitles": "src", 46 | "year": null, 47 | "src": "/tv/Game of Thrones/s1/1.mkv", 48 | "changed": { 49 | "name": "NEW NAME", 50 | "src": "NEW SRC", 51 | "subtitles": "NEW SUB" 52 | } 53 | }, 54 | { 55 | "name": "NOT EXISTS TV", 56 | "season_number": 1, 57 | "tags": [], 58 | "episode_number": 4, 59 | "status": "updated", 60 | "extension": "mp4", 61 | "filename": "2", 62 | "year": null, 63 | "subtitles": "src", 64 | "src": "/tv/Game of Thrones/s1/1.mkv", 65 | "changed": { 66 | "name": "NEW NAME", 67 | "src": "NEW SRC", 68 | "subtitles": "NEW SUB" 69 | } 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/fp/tv/updated_one.json: -------------------------------------------------------------------------------- 1 | { 2 | "tv": [ 3 | { 4 | "name": "Game of Thrones", 5 | "season_number": 1, 6 | "episode_number": 1, 7 | "status": "updated", 8 | "extension": "mkv", 9 | "tags": [], 10 | "year": null, 11 | "filename": "1", 12 | "subtitles": null, 13 | "src": "/tv/Game of Thrones/S1/1.mkv", 14 | "changed": { 15 | "src": "NEW SRC UPDATED", 16 | "subtitles": "NEW SUB UPDATED", 17 | "season_number": 1, 18 | "episode_number": 2 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/imdb/rating.txt: -------------------------------------------------------------------------------- 1 | 5.1 -------------------------------------------------------------------------------- /backend/tests/fixtures/imdb/with-rating.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 | 7,0 9 | 10 | / 11 | 10 12 |
13 |
14 |
15 | 16 |
17 |
-------------------------------------------------------------------------------- /backend/tests/fixtures/imdb/without-rating.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
-------------------------------------------------------------------------------- /backend/tests/fixtures/media/1.mp4: -------------------------------------------------------------------------------- 1 | got s1 e1 mp4 2 | -------------------------------------------------------------------------------- /backend/tests/fixtures/media/2.mp4: -------------------------------------------------------------------------------- 1 | got s1 e2 mp4 2 | -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [] 3 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/movie/alternative_titles.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 68735, 3 | "titles": [ 4 | { 5 | "iso_3166_1": "CA", 6 | "title": "Warcraft: El origen" 7 | }, 8 | { 9 | "iso_3166_1": "SE", 10 | "title": "Warcraft: The Beginning" 11 | }, 12 | { 13 | "iso_3166_1": "DE", 14 | "title": "World of Warcraft" 15 | }, 16 | { 17 | "iso_3166_1": "US", 18 | "title": "Warcraft: El primer encuentro de dos mundos" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/movie/details-failing.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/movie/genres.json: -------------------------------------------------------------------------------- 1 | { 2 | "genres": [ 3 | { 4 | "id": 28, 5 | "name": "Action" 6 | }, 7 | { 8 | "id": 12, 9 | "name": "Adventure" 10 | }, 11 | { 12 | "id": 16, 13 | "name": "Animation" 14 | }, 15 | { 16 | "id": 35, 17 | "name": "Comedy" 18 | }, 19 | { 20 | "id": 80, 21 | "name": "Crime" 22 | }, 23 | { 24 | "id": 99, 25 | "name": "Documentary" 26 | }, 27 | { 28 | "id": 18, 29 | "name": "Drama" 30 | }, 31 | { 32 | "id": 10751, 33 | "name": "Family" 34 | }, 35 | { 36 | "id": 14, 37 | "name": "Fantasy" 38 | }, 39 | { 40 | "id": 36, 41 | "name": "History" 42 | }, 43 | { 44 | "id": 27, 45 | "name": "Horror" 46 | }, 47 | { 48 | "id": 10402, 49 | "name": "Music" 50 | }, 51 | { 52 | "id": 9648, 53 | "name": "Mystery" 54 | }, 55 | { 56 | "id": 10749, 57 | "name": "Romance" 58 | }, 59 | { 60 | "id": 878, 61 | "name": "Science Fiction" 62 | }, 63 | { 64 | "id": 10770, 65 | "name": "TV Movie" 66 | }, 67 | { 68 | "id": 53, 69 | "name": "Thriller" 70 | }, 71 | { 72 | "id": 10752, 73 | "name": "War" 74 | }, 75 | { 76 | "id": 37, 77 | "name": "Western" 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/movie/movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "poster_path": "/jVT1MYp58SSzXc6BKetkGxVS3Ze.jpg", 6 | "release_date": "2016-05-25", 7 | "original_title": "Warcraft", 8 | "genre_ids": [ 9 | 14 10 | ], 11 | "id": 68735, 12 | "media_type": "movie", 13 | "original_language": "en", 14 | "title": "Warcraft: The Beginning", 15 | "vote_average": 1, 16 | "popularity": 1, 17 | "backdrop_path": "xxx", 18 | "overview": "xxx" 19 | }, 20 | { 21 | "poster_path": "/1rxgOwMNwpVP0Hj8R0zhW1CJLfh.jpg", 22 | "release_date": "2015-01-01", 23 | "original_title": "World of Warcraft - Geschichte eines Kult-Spiels", 24 | "genre_ids": [ 25 | 99 26 | ], 27 | "id": 391584, 28 | "media_type": "movie", 29 | "original_language": "de", 30 | "title": "World of Warcraft - Geschichte eines Kult-Spiels", 31 | "vote_average": 1, 32 | "popularity": 1, 33 | "backdrop_path": "xxx", 34 | "overview": "xxx" 35 | }, 36 | { 37 | "poster_path": "/Y6M5JsoYPrtbTnOY1bCOx3IuDi.jpg", 38 | "release_date": "2014-11-08", 39 | "original_title": "World of Warcraft: Looking For Group", 40 | "genre_ids": [ 41 | 99 42 | ], 43 | "id": 301865, 44 | "media_type": "movie", 45 | "original_language": "en", 46 | "title": "World of Warcraft: Looking For Group", 47 | "vote_average": 1, 48 | "popularity": 1, 49 | "backdrop_path": "xxx", 50 | "overview": "xxx" 51 | }, 52 | { 53 | "poster_path": "/fivN0U4HXUMXKtyYfi5S8zdhHTg.jpg", 54 | "release_date": "2010-12-07", 55 | "original_title": "World of Warcraft - Cataclysm - Behind the Scenes", 56 | "genre_ids": [], 57 | "id": 205729, 58 | "media_type": "movie", 59 | "original_language": "en", 60 | "title": "World of Warcraft - Cataclysm - Behind the Scenes", 61 | "vote_average": 1, 62 | "popularity": 1, 63 | "backdrop_path": "xxx", 64 | "overview": "xxx" 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/movie/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "poster_path": "/kmcqlZGaSh20zpTbuoF0Cdn07dT.jpg", 6 | "adult": false, 7 | "overview": "overview", 8 | "release_date": "2009-12-10", 9 | "genre_ids": [ 10 | 28, 11 | 12, 12 | 14, 13 | 878 14 | ], 15 | "id": 19995, 16 | "original_title": "Avatar", 17 | "original_language": "en", 18 | "title": "Avatar: Aufbruch nach Pandora", 19 | "backdrop_path": "/5XPPB44RQGfkBrbJxmtdndKz05n.jpg", 20 | "popularity": 13.117401, 21 | "vote_count": 8721, 22 | "video": false, 23 | "vote_average": 7.1 24 | }, 25 | { 26 | "poster_path": "/famiFiwZPQ3A8WVjKFhCplKrZgr.jpg", 27 | "adult": false, 28 | "overview": "overview", 29 | "release_date": "2011-04-30", 30 | "genre_ids": [ 31 | 27 32 | ], 33 | "id": 282908, 34 | "original_title": "Abataa", 35 | "original_language": "ja", 36 | "title": "Avatar", 37 | "backdrop_path": null, 38 | "popularity": 1.001713, 39 | "vote_count": 0, 40 | "video": false, 41 | "vote_average": 0 42 | } 43 | ], 44 | "total_results": 29, 45 | "total_pages": 2 46 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "poster_path": "\/8uOOycL6r4vqOT8tgw4behO5MmB.jpg", 6 | "popularity": 4.791115, 7 | "id": 33880, 8 | "overview": "The Legend of Korra is an American animated television series that premiered on the Nickelodeon television network in 2012. It was created by Bryan Konietzko and Michael Dante DiMartino as a sequel to their series Avatar: The Last Airbender, which aired on Nickelodeon from 2005 to 2008. Several people involved with creating Avatar, including designer Joaquim Dos Santos and composers Jeremy Zuckerman and Benjamin Wynn, returned to work on The Legend of Korra.\n\nThe series is set in a fictional universe where some people can manipulate, or \"bend\", the elements of water, earth, fire, or air. Only one person, the \"Avatar\", can bend all four elements, and is responsible for maintaining balance in the world. The series follows Avatar Korra, the successor of Aang from the previous series, as she faces political and spiritual unrest in a modernizing world.\n\nThe series, whose style is strongly influenced by Japanese animation, has been a critical and commercial success. It obtained the highest audience total for an animated series in the United States in 2012. The series was praised by reviewers for its high production values and for addressing difficult sociopolitical issues such as social unrest and terrorism. It was initially conceived as a miniseries of 12 episodes, but it is now set to run for 52 episodes separated into four seasons, each of which tells a separate story.", 9 | "backdrop_path": "\/r1oTzR9Ke7pICxe1eiP8ZjoGJju.jpg", 10 | "vote_average": 7.52, 11 | "media_type": "tv", 12 | "first_air_date": "2012-04-14", 13 | "origin_country": [ 14 | "US" 15 | ], 16 | "genre_ids": [ 17 | 10765, 18 | 16, 19 | 18, 20 | 10751 21 | ], 22 | "original_language": "en", 23 | "vote_count": 44, 24 | "name": "The Legend of Korra", 25 | "original_name": "The Legend of Korra" 26 | } 27 | ], 28 | "total_results": 1, 29 | "total_pages": 1 30 | } 31 | -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/tv/alternative_titles.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1399, 3 | "results": [ 4 | { 5 | "iso_3166_1": "CL", 6 | "title": "Game-of-Thrones" 7 | }, 8 | { 9 | "iso_3166_1": "DE", 10 | "title": "GOT" 11 | }, 12 | { 13 | "iso_3166_1": "JP", 14 | "title": "Game of Thrones: The Series" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/tv/episodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "id": 123, 4 | "episodes": [ 5 | { 6 | "episode_number": 1, 7 | "name": "name", 8 | "id": 123, 9 | "season_number": 1 10 | }, 11 | { 12 | "episode_number": 2, 13 | "name": "name", 14 | "id": 123, 15 | "season_number": 1 16 | } 17 | ] 18 | }, 19 | "2": { 20 | "id": 123, 21 | "episodes": [ 22 | { 23 | "episode_number": 1, 24 | "name": "name", 25 | "id": 123, 26 | "season_number": 2 27 | }, 28 | { 29 | "episode_number": 2, 30 | "name": "name", 31 | "id": 123, 32 | "season_number": 2 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/tv/genres.json: -------------------------------------------------------------------------------- 1 | { 2 | "genres": [ 3 | { 4 | "id": 10759, 5 | "name": "Action & Adventure" 6 | }, 7 | { 8 | "id": 16, 9 | "name": "Animation" 10 | }, 11 | { 12 | "id": 35, 13 | "name": "Comedy" 14 | }, 15 | { 16 | "id": 80, 17 | "name": "Crime" 18 | }, 19 | { 20 | "id": 99, 21 | "name": "Documentary" 22 | }, 23 | { 24 | "id": 18, 25 | "name": "Drama" 26 | }, 27 | { 28 | "id": 10751, 29 | "name": "Family" 30 | }, 31 | { 32 | "id": 10762, 33 | "name": "Kids" 34 | }, 35 | { 36 | "id": 9648, 37 | "name": "Mystery" 38 | }, 39 | { 40 | "id": 10763, 41 | "name": "News" 42 | }, 43 | { 44 | "id": 10764, 45 | "name": "Reality" 46 | }, 47 | { 48 | "id": 10765, 49 | "name": "Sci-Fi & Fantasy" 50 | }, 51 | { 52 | "id": 10766, 53 | "name": "Soap" 54 | }, 55 | { 56 | "id": 10767, 57 | "name": "Talk" 58 | }, 59 | { 60 | "id": 10768, 61 | "name": "War & Politics" 62 | }, 63 | { 64 | "id": 37, 65 | "name": "Western" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/tv/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "poster_path": "/4KgScXaTeVZWgsBDJNbDYbeqRjF.jpg", 6 | "popularity": 3.04828, 7 | "id": 246, 8 | "backdrop_path": "/14UEjm0MDQ8C22BqYqdzn3gsAiX.jpg", 9 | "vote_average": 7.8, 10 | "overview": "overview", 11 | "first_air_date": "2005-02-21", 12 | "origin_country": [ 13 | "US" 14 | ], 15 | "genre_ids": [ 16 | 28, 17 | 12, 18 | 16, 19 | 14 20 | ], 21 | "original_language": "en", 22 | "vote_count": 67, 23 | "name": "Avatar: The Last Airbender", 24 | "original_name": "Avatar: The Last Airbender" 25 | }, 26 | { 27 | "poster_path": "/8uOOycL6r4vqOT8tgw4behO5MmB.jpg", 28 | "popularity": 3.52741, 29 | "id": 33880, 30 | "backdrop_path": "/r1oTzR9Ke7pICxe1eiP8ZjoGJju.jpg", 31 | "vote_average": 7.52, 32 | "overview": "overview", 33 | "first_air_date": "2012-04-14", 34 | "origin_country": [ 35 | "US" 36 | ], 37 | "genre_ids": [ 38 | 10765, 39 | 16, 40 | 18, 41 | 10751 42 | ], 43 | "original_language": "en", 44 | "vote_count": 44, 45 | "name": "The Legend of Korra", 46 | "original_name": "The Legend of Korra" 47 | } 48 | ], 49 | "total_results": 2, 50 | "total_pages": 1 51 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/tv/tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "poster_path": "/jIhL6mlT7AblhbHJgEoiBIOUVl1.jpg", 6 | "id": 1399, 7 | "media_type": "tv", 8 | "first_air_date": "2011-04-17", 9 | "genre_ids": [ 10 | 10765, 11 | 10759, 12 | 18 13 | ], 14 | "name": "Game of Thrones", 15 | "original_name": "Game of Thrones", 16 | "vote_average": 1, 17 | "popularity": 1, 18 | "backdrop_path": "xxx", 19 | "overview": "xxx" 20 | }, 21 | { 22 | "poster_path": "/8y1LSfnb8Dfd3XPIrxlpvZ4e8d.jpg", 23 | "release_date": "2011-05-13", 24 | "original_title": "Game of Thrones: Complete History and Lore", 25 | "genre_ids": [ 26 | 16, 27 | 14, 28 | 28, 29 | 12 30 | ], 31 | "id": 269623, 32 | "media_type": "movie", 33 | "original_language": "en", 34 | "title": "Game of Thrones: Complete History and Lore", 35 | "vote_average": 1, 36 | "popularity": 1, 37 | "backdrop_path": "xxx", 38 | "overview": "xxx" 39 | }, 40 | { 41 | "poster_path": null, 42 | "release_date": "2015-02-08", 43 | "original_title": "Game of Thrones: A Day in the Life", 44 | "genre_ids": [ 45 | 99 46 | ], 47 | "id": 340200, 48 | "media_type": "movie", 49 | "original_language": "en", 50 | "title": "Game of Thrones: A Day in the Life", 51 | "vote_average": 1, 52 | "popularity": 1, 53 | "backdrop_path": "xxx", 54 | "overview": "xxx" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /backend/tests/fixtures/tmdb/videos.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 123, 3 | "results": [ 4 | { 5 | "id": "57b1f65492514147e0002da5", 6 | "iso_639_1": "en", 7 | "iso_3166_1": "US", 8 | "key": "qnIhJwhBeqY", 9 | "name": "Trailer", 10 | "site": "YouTube", 11 | "size": 480, 12 | "type": "Trailer" 13 | }, 14 | { 15 | "id": "533ec652c3a3685448000101", 16 | "iso_639_1": "en", 17 | "iso_3166_1": "US", 18 | "key": "6WcJbPlAknw", 19 | "name": "The Lord Of The Rings (Extract) 1978", 20 | "site": "YouTube", 21 | "size": 360, 22 | "type": "Clip" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /bin/install_worker_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Installing Flox worker as a service" 4 | 5 | FLOX_PATH=${1:-$PWD} 6 | echo "Looking for Flox in path: $FLOX_PATH" 7 | 8 | PHP_PATH=${2:-/usr/bin} 9 | echo "Using php binary in: $PHP_PATH" 10 | 11 | mkdir -p $HOME/.config/systemd/user 12 | FILE=$HOME/.config/systemd/user/flox.service 13 | echo "Installing service in: $FILE" 14 | 15 | cat > $FILE <<- EOM 16 | [Unit] 17 | Description=Flox Worker Service 18 | 19 | [Service] 20 | ExecStart=$PHP_PATH/php $FLOX_PATH/backend/artisan queue:work --tries=3 21 | Restart=always 22 | 23 | [Install] 24 | WantedBy=flox.target 25 | EOM 26 | 27 | systemctl --user daemon-reload 28 | echo "Enabling flox.service..." 29 | systemctl --user enable flox.service 30 | echo "Starting flox.service..." 31 | systemctl --user start flox.service 32 | 33 | echo "Done installing Flox service" 34 | 35 | systemctl --user status flox 36 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": [ 4 | "transform-runtime", 5 | ["component", [ 6 | { 7 | "libraryName": "element-ui", 8 | "styleLibraryName": "theme-chalk" 9 | } 10 | ]] 11 | ], 12 | "comments": false 13 | } 14 | -------------------------------------------------------------------------------- /client/app/components/Content/Settings/Api.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 69 | -------------------------------------------------------------------------------- /client/app/components/Content/Settings/Backup.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 76 | -------------------------------------------------------------------------------- /client/app/components/Content/Settings/User.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 78 | -------------------------------------------------------------------------------- /client/app/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 74 | -------------------------------------------------------------------------------- /client/app/components/Login.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /client/app/components/Modal/Index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /client/app/components/Modal/Trailer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 40 | -------------------------------------------------------------------------------- /client/app/components/Search.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 80 | -------------------------------------------------------------------------------- /client/app/config.js: -------------------------------------------------------------------------------- 1 | import http from 'axios'; 2 | http.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('#token').getAttribute('content'); 3 | 4 | const {env, url, uri, auth, language, posterTmdb, posterSubpageTmdb, backdropTmdb} = document.body.dataset; 5 | 6 | const config = { 7 | env, 8 | uri, 9 | url, 10 | auth, 11 | language, 12 | poster: url + '/assets/poster', 13 | backdrop: url + '/assets/backdrop', 14 | posterSubpage: url + '/assets/poster/subpage', 15 | posterTMDB: posterTmdb, 16 | posterSubpageTMDB: posterSubpageTmdb, 17 | backdropTMDB: backdropTmdb, 18 | api: url + '/api' 19 | }; 20 | 21 | window.config = config; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /client/app/helpers/item.js: -------------------------------------------------------------------------------- 1 | import http from 'axios'; 2 | 3 | export default { 4 | methods: { 5 | addToWatchlist(item) { 6 | if(this.auth) { 7 | this.rated = true; 8 | 9 | http.post(`${config.api}/watchlist`, {item}).then(response => { 10 | this.setItem(response.data); 11 | this.rated = false; 12 | }, error => { 13 | alert(error.message); 14 | this.rated = false; 15 | }); 16 | } 17 | }, 18 | 19 | isOn(type, homepage) { 20 | return homepage && homepage.includes(type); 21 | }, 22 | 23 | genreAsString(genre) { 24 | if(typeof genre == 'object') { 25 | return genre.map(item => item.name).join(', '); 26 | } 27 | 28 | return genre 29 | }, 30 | 31 | displaySeason(item) { 32 | return item.media_type == 'tv' && item.rating != null && item.tmdb_id && ! item.watchlist; 33 | }, 34 | 35 | openSeasonModal(item) { 36 | const data = { 37 | tmdb_id: item.tmdb_id, 38 | title: item.title 39 | }; 40 | 41 | this.fetchEpisodes(data); 42 | 43 | this.OPEN_MODAL({ 44 | type: 'season', 45 | data 46 | }); 47 | }, 48 | 49 | addZero(item) { 50 | if(item < 10) { 51 | return '0' + item; 52 | } 53 | 54 | return item; 55 | }, 56 | 57 | intToFloat(int) { 58 | if(int) { 59 | return parseFloat(int).toFixed(1); 60 | } 61 | 62 | return null; 63 | } 64 | }, 65 | 66 | computed: { 67 | season() { 68 | if(this.latestEpisode) { 69 | return this.addZero(this.latestEpisode.season_number); 70 | } 71 | }, 72 | 73 | episode() { 74 | if(this.latestEpisode) { 75 | return this.addZero(this.latestEpisode.episode_number); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /client/app/helpers/misc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | // http://stackoverflow.com/a/24559613 4 | scrollToTop(scrollDuration = 300) { 5 | let cosParameter = window.scrollY / 2; 6 | let scrollCount = 0; 7 | let oldTimestamp = performance.now(); 8 | 9 | function step(newTimestamp) { 10 | scrollCount += Math.PI / (scrollDuration / (newTimestamp - oldTimestamp)); 11 | 12 | if(scrollCount >= Math.PI) window.scrollTo(0, 0); 13 | if(window.scrollY === 0) return; 14 | 15 | window.scrollTo(0, Math.round(cosParameter + cosParameter * Math.cos(scrollCount))); 16 | oldTimestamp = newTimestamp; 17 | window.requestAnimationFrame(step); 18 | } 19 | 20 | window.requestAnimationFrame(step); 21 | }, 22 | 23 | suggestionsUri(item) { 24 | return `/suggestions?for=${item.tmdb_id}&name=${item.title}&type=${item.media_type}`; 25 | }, 26 | 27 | // Language helper 28 | lang(text) { 29 | const language = JSON.parse(config.language); 30 | 31 | return language[text] || text; 32 | }, 33 | 34 | formatLocaleDate(date) { 35 | const language = navigator.language || navigator.userLanguage; 36 | 37 | return date.toLocaleDateString(language, { 38 | year: '2-digit', 39 | month: '2-digit', 40 | day: '2-digit' 41 | }); 42 | }, 43 | 44 | isSubpage() { 45 | return this.$route.name.includes('subpage'); 46 | } 47 | }, 48 | 49 | computed: { 50 | displayHeader() { 51 | if(this.isSubpage()) { 52 | return this.itemLoadedSubpage; 53 | } 54 | 55 | return true; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /client/app/routes.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | import config from './config'; 5 | 6 | import Content from './components/Content/Content.vue'; 7 | import SearchContent from './components/Content/SearchContent.vue'; 8 | import Settings from './components/Content/Settings/Index.vue'; 9 | import TMDBContent from './components/Content/TMDBContent.vue'; 10 | import Subpage from './components/Content/Subpage.vue'; 11 | import Calendar from './components/Content/Calendar.vue'; 12 | 13 | Vue.use(Router); 14 | 15 | export default new Router({ 16 | mode: 'history', 17 | base: config.uri, 18 | routes: [ 19 | { path: '/', component: Content, name: 'home' }, 20 | 21 | // todo: use props for media type 22 | { path: '/movies', component: Content, name: 'movie' }, 23 | { path: '/tv', component: Content, name: 'tv' }, 24 | { path: '/watchlist/:type?', component: Content, name: 'watchlist' }, 25 | 26 | { path: '/movies/:tmdbId/:slug?', component: Subpage, name: 'subpage-movie', props: {mediaType: 'movie'} }, 27 | { path: '/tv/:tmdbId/:slug?', component: Subpage, name: 'subpage-tv', props: {mediaType: 'tv'} }, 28 | 29 | { path: '/search', component: SearchContent, name: 'search' }, 30 | { path: '/settings', component: Settings, name: 'settings' }, 31 | { path: '/suggestions', component: TMDBContent, name: 'suggestions' }, 32 | { path: '/trending', component: TMDBContent, name: 'trending' }, 33 | { path: '/upcoming', component: TMDBContent, name: 'upcoming' }, 34 | { path: '/now-playing', component: TMDBContent, name: 'now-playing' }, 35 | { path: '/genre/:genre', component: TMDBContent, name: 'genre' }, 36 | { path: '/calendar', component: Calendar, name: 'calendar' }, 37 | 38 | { path: '*', redirect: '/' } 39 | ] 40 | }); 41 | -------------------------------------------------------------------------------- /client/app/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex' 3 | 4 | import * as actions from './actions'; 5 | import mutations from './mutations'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | state: { 11 | filters: [ 12 | 'last seen', 13 | 'own rating', 14 | 'title', 15 | 'release', 16 | 'tmdb rating', 17 | 'imdb rating' 18 | ], 19 | showFilters: false, 20 | items: [], 21 | searchTitle: '', 22 | userFilter: '', 23 | userSortDirection: '', 24 | loading: false, 25 | clickedMoreLoading: false, 26 | paginator: null, 27 | colorScheme: '', 28 | overlay: false, 29 | modalData: {}, 30 | loadingModalData: true, 31 | seasonActiveModal: 1, 32 | modalType: '', 33 | itemLoadedSubpage: false 34 | }, 35 | mutations, 36 | actions 37 | }); -------------------------------------------------------------------------------- /client/app/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as type from './types'; 2 | 3 | export default { 4 | [type.SET_SEARCH_TITLE](state, title) { 5 | state.searchTitle = title; 6 | }, 7 | 8 | [type.SET_USER_FILTER](state, filter) { 9 | state.userFilter = filter; 10 | }, 11 | 12 | [type.SET_USER_SORT_DIRECTION](state, direction) { 13 | state.userSortDirection = direction; 14 | }, 15 | 16 | [type.SET_ITEMS](state, items) { 17 | state.items = items; 18 | }, 19 | 20 | [type.PUSH_TO_ITEMS](state, items) { 21 | state.items.push(...items); 22 | }, 23 | 24 | [type.SET_LOADING](state, loading) { 25 | state.loading = loading; 26 | }, 27 | 28 | [type.SET_PAGINATOR](state, data) { 29 | state.paginator = data; 30 | }, 31 | 32 | [type.SET_CLICKED_LOADING](state, loading) { 33 | state.clickedMoreLoading = loading; 34 | }, 35 | 36 | [type.SET_COLOR_SCHEME](state, color) { 37 | state.colorScheme = color; 38 | }, 39 | 40 | [type.CLOSE_MODAL](state) { 41 | state.modalType = false; 42 | state.overlay = false; 43 | state.seasonActiveModal = 1; 44 | document.body.classList.remove('open-modal'); 45 | }, 46 | 47 | [type.OPEN_MODAL](state, data) { 48 | state.overlay = true; 49 | state.modalType = data.type; 50 | state.modalData = data.data; 51 | document.body.classList.add('open-modal'); 52 | }, 53 | 54 | [type.SET_LOADING_MODAL_DATA](state, bool) { 55 | state.loadingModalData = bool; 56 | }, 57 | 58 | [type.SET_SEASON_ACTIVE_MODAL](state, season) { 59 | state.seasonActiveModal = season; 60 | }, 61 | 62 | [type.SET_MODAL_DATA](state, data) { 63 | state.modalData = data; 64 | }, 65 | 66 | [type.SET_ITEM_LOADED_SUBPAGE](state, bool) { 67 | state.itemLoadedSubpage = bool; 68 | }, 69 | 70 | [type.SET_SHOW_FILTERS](state, bool) { 71 | state.showFilters = bool; 72 | } 73 | } -------------------------------------------------------------------------------- /client/app/store/types.js: -------------------------------------------------------------------------------- 1 | export const SET_SEARCH_TITLE = 'SET_SEARCH_TITLE'; 2 | export const SET_USER_FILTER = 'SET_USER_FILTER'; 3 | export const SET_USER_SORT_DIRECTION = 'SET_USER_SORT_DIRECTION'; 4 | export const SET_ITEMS = 'SET_ITEMS'; 5 | export const PUSH_TO_ITEMS = 'PUSH_TO_ITEMS'; 6 | export const SET_LOADING = 'SET_LOADING'; 7 | export const SET_PAGINATOR = 'SET_PAGINATOR'; 8 | export const SET_CLICKED_LOADING = 'SET_CLICKED_LOADING'; 9 | export const SET_COLOR_SCHEME = 'SET_COLOR_SCHEME'; 10 | export const CLOSE_MODAL = 'CLOSE_MODAL'; 11 | export const OPEN_MODAL = 'OPEN_MODAL'; 12 | export const SET_SEASON_ACTIVE_MODAL = 'SET_SEASON_ACTIVE_MODAL'; 13 | export const SET_LOADING_MODAL_DATA = 'SET_LOADING_MODAL_DATA'; 14 | export const SET_MODAL_DATA = 'SET_MODAL_DATA'; 15 | export const SET_ITEM_LOADED_SUBPAGE = 'SET_ITEM_LOADED_SUBPAGE'; 16 | export const SET_SHOW_FILTERS = 'SET_SHOW_FILTERS'; -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", 5 | "dev": "webpack -w --progress --hide-modules", 6 | "mail": "./node_modules/.bin/mjml ./resources/mails/templates/*.mjml.blade.php -o ./resources/mails/compiled" 7 | }, 8 | "dependencies": { 9 | "axios": "^0.18.1", 10 | "babel-runtime": "^6.26.0", 11 | "dayjs": "^1.7.5", 12 | "debounce": "^1.1.0", 13 | "element-ui": "^2.0.10", 14 | "v-hotkey": "^0.2.3", 15 | "vue": "^2.5.2", 16 | "vue-checkbox-radio": "^0.6.0", 17 | "vue-router": "^3.0.1", 18 | "vue-simple-calendar": "4.1.*", 19 | "vuex": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^7.1.6", 23 | "babel-core": "^6.26.0", 24 | "babel-loader": "^7.1.2", 25 | "babel-plugin-component": "^1.0.0", 26 | "babel-plugin-transform-runtime": "^6.23.0", 27 | "babel-preset-es2015": "^6.24.1", 28 | "babel-preset-stage-2": "^6.24.1", 29 | "cross-env": "^5.1.1", 30 | "mjml": "^4.2.0", 31 | "css-loader": "^0.28.7", 32 | "extract-text-webpack-plugin": "^3.0.2", 33 | "file-loader": "^1.1.5", 34 | "lost": "^8.2.0", 35 | "node-sass": "^4.13", 36 | "postcss-loader": "^2.0.8", 37 | "sass-loader": "^6.0.6", 38 | "style-loader": "^0.19.0", 39 | "url-loader": "^0.6.2", 40 | "vue-html-loader": "^1.2.4", 41 | "vue-loader": "^13.3.0", 42 | "vue-template-compiler": "^2.5.2", 43 | "webpack": "^3.8.1", 44 | "webpack-cli": "^3.1.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | require('lost') 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /client/resources/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flox 10 | 11 | 12 | 13 | 14 | 25 | 26 |
27 | @if(Request::is('login')) 28 | 29 | @else 30 | 31 | 32 | 33 | 34 | @endif 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /client/resources/languages/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "Usuário", 3 | "password": "Senha", 4 | "login button": "Entrar", 5 | "login error": "Usuário ou senha incorreta", 6 | 7 | "suggestions": "Sugestões", 8 | "delete movie": "Deletar", 9 | "confirm delete": "Você tem certeza?", 10 | "search": "Pesquisar", 11 | "search or add": "Pesquisar ou adicionar", 12 | "load more": "Carregar mais", 13 | "nothing found": "Nada foi encontrado", 14 | 15 | "trending": "Tendência", 16 | "upcoming": "Próximos", 17 | "tv": "TV", 18 | "movies": "Filmes", 19 | "last seen": "Últimos vistos", 20 | "best rated": "Melhores avaliados", 21 | "change color": "Alterar cores", 22 | 23 | "settings": "Configurações", 24 | "logout": "Sair", 25 | "headline user": "Usuário", 26 | "headline export import": "Exportar / Importar", 27 | "headline misc": "Diversos", 28 | "save button": "Salvar", 29 | "password message": "Deixe sua senha em branco se você não deseja mudá-la", 30 | "success message": "Mudança bem sucedida", 31 | "export button": "Exportar filmes", 32 | "import button": "Importar filmes", 33 | "or divider": "OU", 34 | "update genre": "Atualizar gênero", 35 | "sync scout": "Sincronizar com Laravel Scout", 36 | "display genre": "Exibir gêneros", 37 | "display date": "Exibir data", 38 | "success import": "Filmes importados com sucesso", 39 | "import warn": "Todos os filmes serão substituídos. Certifique-se de ter feito um backup!", 40 | "genre message": "Para atualizar uma versão antiga da flox", 41 | "current version": "Versão atual:", 42 | "new update": "Existe uma nova atualização para a flox!", 43 | "no update": "Nada para atualizar", 44 | "checking update": "Verificando atualizações...", 45 | "spoiler": "Proteção contra Spoiler para nomes de episódios" 46 | } 47 | -------------------------------------------------------------------------------- /client/resources/sass/_base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: 'Open Sans', sans-serif; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | body { 9 | overflow-y: scroll; 10 | background: #fff; 11 | 12 | &.dark { 13 | background: #1c1c1c; 14 | } 15 | 16 | &.open-modal { 17 | overflow: hidden; 18 | } 19 | } 20 | 21 | html { 22 | -webkit-text-size-adjust: 100%; 23 | } 24 | 25 | input { 26 | -webkit-appearance: none !important; 27 | } 28 | 29 | .wrap, 30 | .wrap-content, 31 | .content-submenu { 32 | lost-center: 1300px 20px; 33 | width: 100%; 34 | } 35 | 36 | .wrap-content, 37 | .content-submenu { 38 | @include media(1) { lost-center: 1120px 20px; } 39 | @include media(2) { lost-center: 960px 20px; } 40 | @include media(3) { lost-center: 800px 20px; } 41 | @include media(4) { lost-center: 620px 20px; } 42 | @include media(6) { lost-center: 290px 20px; } 43 | } 44 | 45 | input, 46 | a { 47 | outline: 0 48 | } 49 | 50 | ::selection { 51 | background: rgba($main1, .99); 52 | color: #fff; 53 | } 54 | 55 | @keyframes blink { 56 | 0% { opacity: 1; } 57 | 50% { opacity: .3; } 58 | 100% { opacity: 1; } 59 | } 60 | -------------------------------------------------------------------------------- /client/resources/sass/_element-ui.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/element-ui/lib/theme-chalk/checkbox.css'; 2 | 3 | .element-ui-checkbox { 4 | .el-checkbox__inner { 5 | background: transparent; 6 | border-radius: 0 !important; 7 | transition: none; 8 | border: 1px solid darken(#626262, 10%) !important; 9 | 10 | &:after { 11 | transition: none; 12 | } 13 | } 14 | 15 | .el-checkbox__label { 16 | color: #888 !important; 17 | padding-left: 7px; 18 | 19 | .dark & { 20 | color: #626262 !important; 21 | } 22 | } 23 | 24 | .el-checkbox__input.is-checked .el-checkbox__inner, 25 | .el-checkbox__input.is-indeterminate .el-checkbox__inner { 26 | border: 1px solid transparent !important; 27 | background: $main2; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import 2 | 3 | 'normalize', 4 | 'misc', 5 | 'sprite', 6 | 'shake', 7 | 'base', 8 | 9 | 'element-ui', 10 | 11 | 'components/header', 12 | 'components/search', 13 | 'components/content', 14 | 'components/subpage', 15 | 'components/login', 16 | 'components/footer', 17 | 'components/calendar', 18 | 'components/modal', 19 | 'components/lists'; 20 | -------------------------------------------------------------------------------- /client/resources/sass/components/_footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | padding: 40px 0; 3 | width: 100%; 4 | float: left; 5 | background: $main2; 6 | background: $gradient; 7 | 8 | .open-modal & { 9 | padding: 40px 16px 0 0; 10 | } 11 | 12 | .dark & { 13 | opacity: .9; 14 | } 15 | } 16 | 17 | .attribution { 18 | color: #fff; 19 | float: left; 20 | 21 | @include media(4) { 22 | font-size: 14px; 23 | } 24 | } 25 | 26 | .tmdb-logo { 27 | @include media(4) { 28 | width: 100%; 29 | float: left; 30 | } 31 | } 32 | 33 | .footer-actions { 34 | float: right; 35 | 36 | @include media(3) { 37 | float: left; 38 | clear: both; 39 | margin: 20px 0 0 0; 40 | } 41 | } 42 | 43 | .icon-tmdb { 44 | background: url(../../../public/assets/img/tmdb.png); 45 | width: 139px; 46 | height: 18px; 47 | float: left; 48 | margin: 3px 10px 0 0; 49 | 50 | &:active { 51 | opacity: .6; 52 | } 53 | } 54 | 55 | .icon-github { 56 | background: url(../../../public/assets/img/github.png); 57 | width: 33px; 58 | height: 27px; 59 | float: right; 60 | 61 | @include media(3) { 62 | float: left; 63 | //clear: both; 64 | //margin: 30px 0 0 0; 65 | } 66 | 67 | &:active { 68 | opacity: .6; 69 | } 70 | } 71 | 72 | .icon-constrast { 73 | float: right; 74 | width: 30px; 75 | height: 30px; 76 | margin: 0 0 0 20px; 77 | cursor: pointer; 78 | padding: 8px; 79 | 80 | &:active { 81 | opacity: .8; 82 | } 83 | 84 | i { 85 | background: darken($dark, 20%); 86 | border-radius: 100%; 87 | width: 100%; 88 | height: 100%; 89 | float: left; 90 | 91 | .dark & { 92 | background: #fff; 93 | } 94 | } 95 | } 96 | 97 | .sub-links { 98 | float: left; 99 | clear: both; 100 | } 101 | 102 | .login-btn { 103 | float: left; 104 | color: #fff; 105 | text-decoration: none; 106 | margin: 20px 20px 0 0; 107 | 108 | &:active { 109 | opacity: .6; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /client/resources/sass/components/_lists.scss: -------------------------------------------------------------------------------- 1 | .list-item-wrap { 2 | position: relative; 3 | margin: 0 0 30px 0; 4 | background: $gradient; 5 | height: 220px; 6 | cursor: pointer; 7 | box-shadow: 0 12px 15px 0 rgba(0, 0, 0, .5); 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: flex-end; 11 | padding: 20px; 12 | 13 | lost-column: 1/3; 14 | 15 | @include transition(box-shadow); 16 | @include media(3) { lost-column: 1; } 17 | 18 | &:hover { 19 | //box-shadow: 0 0 2px 2px $main2; 20 | } 21 | 22 | &:active { 23 | //box-shadow: 0 0 2px 2px $main1; 24 | } 25 | } 26 | 27 | .list-item-teaser-image { 28 | width: 100%; 29 | height: 100%; 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | background-size: cover; 34 | background-position: 100% 25%; 35 | 36 | opacity: .2; 37 | 38 | transition: opacity 1s ease 0s; 39 | 40 | .active & { 41 | opacity: .2; 42 | } 43 | } 44 | 45 | .list-item-title { 46 | color: #fff; 47 | font-size: 30px; 48 | float: left; 49 | position: relative; 50 | z-index: 10; 51 | } 52 | 53 | .list-item-amount { 54 | float: left; 55 | clear: both; 56 | font-size: 16px; 57 | opacity: .7; 58 | color: #fff; 59 | } 60 | -------------------------------------------------------------------------------- /client/resources/sass/components/_login.scss: -------------------------------------------------------------------------------- 1 | .login-wrap { 2 | lost-center: 320px 20px; 3 | 4 | @include media(4) { 5 | width: 100%; 6 | } 7 | } 8 | 9 | .top-bar { 10 | float: left; 11 | width: 100%; 12 | height: 30px; 13 | background: $main2; 14 | background: $gradient; 15 | } 16 | 17 | .logo-login { 18 | display: block; 19 | margin: 30vh auto 50px auto; 20 | } 21 | 22 | .login-form { 23 | float: left; 24 | max-width: 300px; 25 | width: 100%; 26 | 27 | input[type="text"], 28 | input[type="email"], 29 | input[type="password"] { 30 | float: left; 31 | width: 100%; 32 | font-size: 15px; 33 | margin: 0 0 5px 0; 34 | background: #333; 35 | padding: 12px; 36 | color: #fff; 37 | border: 0; 38 | } 39 | 40 | input[type="submit"] { 41 | background: $main2; 42 | background: $gradient; 43 | color: #fff; 44 | font-size: 17px; 45 | border: 0; 46 | text-transform: uppercase; 47 | padding: 8px 20px; 48 | cursor: pointer; 49 | float: left; 50 | 51 | &:active { 52 | opacity: .8; 53 | } 54 | } 55 | } 56 | 57 | .login-error { 58 | float: left; 59 | height: 20px; 60 | width: 100%; 61 | margin: 10px 0; 62 | 63 | span { 64 | float: left; 65 | color: $rating3; 66 | font-size: 14px; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: { 7 | app: './app/app.js', 8 | vendor: ['vue', 'axios', 'vuex', 'debounce', 'vue-router'] 9 | }, 10 | watchOptions: { 11 | poll: true 12 | }, 13 | output: { 14 | path: path.resolve('../public/assets'), 15 | filename: 'app.js' 16 | }, 17 | resolve: { 18 | alias: { 19 | vue$: 'vue/dist/vue.common' 20 | } 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.vue$/, 26 | use: 'vue-loader' 27 | }, 28 | { 29 | test: /\.js$/, 30 | use: 'babel-loader', 31 | exclude: /node_modules/ 32 | }, 33 | { 34 | test: /\.(png|jpg|svg|woff|woff2|eot|ttf)$/, 35 | use: { 36 | loader: 'url-loader', 37 | options: { 38 | limit: 10000, 39 | name: 'img/[name].[ext]', 40 | emitFile: false 41 | } 42 | } 43 | }, 44 | { 45 | test: /\.(scss|css)$/, 46 | use: ExtractTextPlugin.extract({ 47 | fallback: 'style-loader', 48 | use: ['css-loader', 'postcss-loader', 'sass-loader'] 49 | }) 50 | } 51 | ] 52 | }, 53 | plugins: [ 54 | new webpack.optimize.CommonsChunkPlugin({name: 'vendor', filename: 'vendor.js'}), 55 | new ExtractTextPlugin('app.css') 56 | ] 57 | }; 58 | 59 | if(process.env.NODE_ENV === 'production') { 60 | module.exports.plugins = (module.exports.plugins || []).concat([ 61 | new webpack.DefinePlugin({ 62 | 'process.env': { 63 | NODE_ENV: '"production"' 64 | } 65 | }), 66 | new webpack.optimize.UglifyJsPlugin({ 67 | compress: { 68 | warnings: false 69 | } 70 | }) 71 | ]) 72 | } 73 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Redirect Trailing Slashes If Not A Folder... 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteRule ^(.*)/$ /$1 [L,R=301] 11 | 12 | # Handle Front Controller... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteRule ^ index.php [L] 16 | 17 | # Handle Authorization Header 18 | RewriteCond %{HTTP:Authorization} . 19 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 20 | 21 | -------------------------------------------------------------------------------- /public/assets/backdrop/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/backdrop/.gitkeep -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/favicon.ico -------------------------------------------------------------------------------- /public/assets/img/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/add.png -------------------------------------------------------------------------------- /public/assets/img/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/clock.png -------------------------------------------------------------------------------- /public/assets/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/close.png -------------------------------------------------------------------------------- /public/assets/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/github.png -------------------------------------------------------------------------------- /public/assets/img/hamburger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/hamburger.png -------------------------------------------------------------------------------- /public/assets/img/has-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/has-src.png -------------------------------------------------------------------------------- /public/assets/img/is-finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/is-finished.png -------------------------------------------------------------------------------- /public/assets/img/logo-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/logo-login.png -------------------------------------------------------------------------------- /public/assets/img/logo-small-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/logo-small-light.png -------------------------------------------------------------------------------- /public/assets/img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/logo-small.png -------------------------------------------------------------------------------- /public/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/logo.png -------------------------------------------------------------------------------- /public/assets/img/no-image-subpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/no-image-subpage.png -------------------------------------------------------------------------------- /public/assets/img/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/no-image.png -------------------------------------------------------------------------------- /public/assets/img/rating-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/rating-0.png -------------------------------------------------------------------------------- /public/assets/img/rating-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/rating-1.png -------------------------------------------------------------------------------- /public/assets/img/rating-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/rating-2.png -------------------------------------------------------------------------------- /public/assets/img/rating-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/rating-3.png -------------------------------------------------------------------------------- /public/assets/img/search-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/search-dark.png -------------------------------------------------------------------------------- /public/assets/img/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/search.png -------------------------------------------------------------------------------- /public/assets/img/seen-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/seen-active.png -------------------------------------------------------------------------------- /public/assets/img/seen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/seen.png -------------------------------------------------------------------------------- /public/assets/img/suggest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/suggest.png -------------------------------------------------------------------------------- /public/assets/img/tmdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/tmdb.png -------------------------------------------------------------------------------- /public/assets/img/trailer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/trailer.png -------------------------------------------------------------------------------- /public/assets/img/watchlist-remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/watchlist-remove.png -------------------------------------------------------------------------------- /public/assets/img/watchlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/img/watchlist.png -------------------------------------------------------------------------------- /public/assets/poster/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/poster/.gitkeep -------------------------------------------------------------------------------- /public/assets/poster/subpage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/poster/subpage/.gitkeep -------------------------------------------------------------------------------- /public/assets/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/assets/screenshot.jpg -------------------------------------------------------------------------------- /public/exports/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfake/flox/3043730c521a2035e4fd8411443be19f1a1d97de/public/exports/.gitkeep -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Register The Auto Loader 13 | |-------------------------------------------------------------------------- 14 | | 15 | | Composer provides a convenient, automatically generated class loader for 16 | | our application. We just need to utilize it! We'll simply require it 17 | | into the script here so that we don't have to worry about manual 18 | | loading any of our classes later on. It feels nice to relax. 19 | | 20 | */ 21 | 22 | require __DIR__.'/../backend/bootstrap/autoload.php'; 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Turn On The Lights 27 | |-------------------------------------------------------------------------- 28 | | 29 | | We need to illuminate PHP development, so let us turn on the lights. 30 | | This bootstraps the framework and gets it ready for use, then it 31 | | will load up this application so that we can run it and send 32 | | the responses back to the browser and delight our users. 33 | | 34 | */ 35 | 36 | $app = require_once __DIR__.'/../backend/bootstrap/app.php'; 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Run The Application 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Once we have the application, we can handle the incoming request 44 | | through the kernel, and send the associated response back to 45 | | the client's browser allowing them to enjoy the creative 46 | | and wonderful application we have prepared for them. 47 | | 48 | */ 49 | 50 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 51 | 52 | $response = $kernel->handle( 53 | $request = Illuminate\Http\Request::capture() 54 | ); 55 | 56 | $response->send(); 57 | 58 | $kernel->terminate($request, $response); 59 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------