├── .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 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
Data cannot be changed in the demo
5 |
6 |
12 |
13 |
14 |
15 |
16 |
69 |
--------------------------------------------------------------------------------
/client/app/components/Content/Settings/Backup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
76 |
--------------------------------------------------------------------------------
/client/app/components/Content/Settings/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Data cannot be changed in the demo
5 |
12 |
13 |
14 |
15 |
16 |
78 |
--------------------------------------------------------------------------------
/client/app/components/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
74 |
--------------------------------------------------------------------------------
/client/app/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |

7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/client/app/components/Modal/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/app/components/Modal/Trailer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
40 |
--------------------------------------------------------------------------------
/client/app/components/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 | {{ lang('suggestions for') }} {{ suggestionsFor }}
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------