├── .gitignore ├── sources ├── lib │ └── .gitignore └── app │ ├── dict │ ├── settings │ │ ├── app.ini │ │ ├── console.ini │ │ └── f3.ini │ └── l10n │ │ └── en.php │ ├── controllers │ ├── controller.php │ ├── home.php │ ├── sign.php │ ├── link.php │ ├── admin.php │ ├── links.php │ ├── downloads.php │ └── install.php │ ├── models │ ├── objects │ │ ├── user.php │ │ ├── mobject.php │ │ ├── media.php │ │ └── download.php │ ├── media.php │ ├── users.php │ ├── config.php │ ├── downloads.php │ └── model.php │ ├── views │ ├── link.html │ ├── footer.htm │ ├── server-infos.htm │ ├── links-browse.html │ ├── login.html │ ├── links-download.html │ ├── home.html │ ├── header.htm │ ├── links-add.html │ ├── install.html │ ├── downloads.html │ ├── link.htm │ └── admin.html │ ├── lib │ ├── utils.php │ ├── traits │ │ └── readaccess.php │ ├── validations │ │ ├── rules │ │ │ ├── youtubedl.php │ │ │ └── phpversion.php │ │ └── exceptions │ │ │ ├── youtubedlexception.php │ │ │ └── phpversionexception.php │ ├── exception.php │ ├── keyscounter.php │ ├── human.php │ ├── signedinuser.php │ ├── command.php │ ├── session.php │ ├── install.php │ ├── alerter.php │ ├── logger.php │ ├── debugger.php │ ├── downloadsmanager.php │ ├── formatinfo.php │ ├── process.php │ └── youtubedl.php │ ├── bin │ ├── bin.php │ └── downloadsmanager.php │ └── app.php ├── temp └── .gitignore ├── resources ├── logs │ └── .gitignore ├── databases │ └── .gitignore └── sessions │ └── .gitignore ├── public ├── downloads │ └── .gitignore ├── css │ ├── code.css │ ├── main.css │ ├── gfonts.css │ └── font-awesome.min.css ├── debugbar ├── img │ ├── admin.jpg │ ├── home.jpg │ ├── links.jpg │ └── list.jpg ├── index.php ├── fonts │ ├── glyphicons │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── sourcesanspro │ │ └── v9 │ │ ├── M2Jd71oPJhLKp0zdtTvoM40tgx99jmYGv_xzYuwd1rU.woff2 │ │ ├── M2Jd71oPJhLKp0zdtTvoM7YHq4FgHI02B8rPccK0FJQ.woff2 │ │ ├── M2Jd71oPJhLKp0zdtTvoMxgy2Fsj5sj3EzlXpqVXRKo.woff2 │ │ ├── ODelI1aHBYDBqgeIAH2zlC2Q8seG17bfDXYR_jUsrzg.woff2 │ │ ├── ODelI1aHBYDBqgeIAH2zlNOAHFN6BivSraYkjhveRHY.woff2 │ │ ├── ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2 │ │ ├── toadOcfmlt9b38dHJxOBGDRVvBvQIc1z78c__uoBcyI.woff2 │ │ ├── toadOcfmlt9b38dHJxOBGD_j0nMiB9fPhg_k1wdK2h0.woff2 │ │ ├── toadOcfmlt9b38dHJxOBGDovqjS_dXPZszO_XltPdNg.woff2 │ │ ├── toadOcfmlt9b38dHJxOBGEo0As1BFRXtCDhS66znb_k.woff2 │ │ ├── toadOcfmlt9b38dHJxOBGFxe-GPfKKFmiXaJ_Q0GFr8.woff2 │ │ └── toadOcfmlt9b38dHJxOBGOode0-EuMkY--TSyExeINg.woff2 ├── .htaccess └── js │ └── bootstrap.min.js ├── mdc ├── composer.json ├── README.md ├── LICENSE └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /sources/lib/.gitignore: -------------------------------------------------------------------------------- 1 | composer/ 2 | -------------------------------------------------------------------------------- /temp/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | /*/ 3 | !/.gitignore 4 | -------------------------------------------------------------------------------- /resources/logs/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | /*/ 3 | !/.gitignore 4 | -------------------------------------------------------------------------------- /public/downloads/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | /*/ 3 | !/.gitignore 4 | -------------------------------------------------------------------------------- /resources/databases/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | /*/ 3 | !/.gitignore 4 | -------------------------------------------------------------------------------- /resources/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | /*/ 3 | !/.gitignore 4 | -------------------------------------------------------------------------------- /public/css/code.css: -------------------------------------------------------------------------------- 1 | ../../sources/lib/composer/bcosca/fatfree-core/code.css -------------------------------------------------------------------------------- /public/debugbar: -------------------------------------------------------------------------------- 1 | ../sources/lib/composer/maximebf/debugbar/src/DebugBar/Resources/ -------------------------------------------------------------------------------- /public/img/admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/img/admin.jpg -------------------------------------------------------------------------------- /public/img/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/img/home.jpg -------------------------------------------------------------------------------- /public/img/links.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/img/links.jpg -------------------------------------------------------------------------------- /public/img/list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/img/list.jpg -------------------------------------------------------------------------------- /sources/app/dict/settings/app.ini: -------------------------------------------------------------------------------- 1 | ; === App globals === 2 | [App] 3 | Name=MediaDownloader 4 | -------------------------------------------------------------------------------- /sources/app/controllers/controller.php: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | Run(); -------------------------------------------------------------------------------- /public/fonts/glyphicons/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/glyphicons/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/glyphicons/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/glyphicons/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /sources/app/views/link.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /public/fonts/glyphicons/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/glyphicons/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /mdc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | RunConsole(); -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoM40tgx99jmYGv_xzYuwd1rU.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoM40tgx99jmYGv_xzYuwd1rU.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoM7YHq4FgHI02B8rPccK0FJQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoM7YHq4FgHI02B8rPccK0FJQ.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoMxgy2Fsj5sj3EzlXpqVXRKo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoMxgy2Fsj5sj3EzlXpqVXRKo.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlC2Q8seG17bfDXYR_jUsrzg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlC2Q8seG17bfDXYR_jUsrzg.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNOAHFN6BivSraYkjhveRHY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNOAHFN6BivSraYkjhveRHY.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGDRVvBvQIc1z78c__uoBcyI.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGDRVvBvQIc1z78c__uoBcyI.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGD_j0nMiB9fPhg_k1wdK2h0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGD_j0nMiB9fPhg_k1wdK2h0.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGDovqjS_dXPZszO_XltPdNg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGDovqjS_dXPZszO_XltPdNg.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGEo0As1BFRXtCDhS66znb_k.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGEo0As1BFRXtCDhS66znb_k.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGFxe-GPfKKFmiXaJ_Q0GFr8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGFxe-GPfKKFmiXaJ_Q0GFr8.woff2 -------------------------------------------------------------------------------- /public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGOode0-EuMkY--TSyExeINg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kallys/MediaDownloader/HEAD/public/fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGOode0-EuMkY--TSyExeINg.woff2 -------------------------------------------------------------------------------- /sources/app/controllers/home.php: -------------------------------------------------------------------------------- 1 | render('home.html'); 10 | } 11 | } -------------------------------------------------------------------------------- /sources/app/lib/utils.php: -------------------------------------------------------------------------------- 1 | __isset($name) ? $this->{$name} : null; 10 | } 11 | 12 | public function __isset($name) 13 | { 14 | return isset($this->{$name}); 15 | } 16 | } 17 | 18 | ?> 19 | -------------------------------------------------------------------------------- /sources/app/bin/bin.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | {{ \App\Lib\Debugger::instance()->Render() }} 10 | 11 | -------------------------------------------------------------------------------- /sources/app/lib/validations/rules/youtubedl.php: -------------------------------------------------------------------------------- 1 | mapper = $mapper; 13 | } 14 | 15 | public function __isset(string $name) 16 | { 17 | return $this->mapper->exists($name) || $name === '_id'; 18 | } 19 | 20 | public function __get(string $name) 21 | { 22 | return $this->mapper->get($name); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sources/app/lib/validations/exceptions/youtubedlexception.php: -------------------------------------------------------------------------------- 1 | [ 11 | self::STANDARD => '{{name}} must be a path to youtube-dl binary.', 12 | ], 13 | self::MODE_NEGATIVE => [ 14 | self::STANDARD => '{{name}} must not be a path to youtube-dl binary.', 15 | ], 16 | ]; 17 | } -------------------------------------------------------------------------------- /sources/app/lib/exception.php: -------------------------------------------------------------------------------- 1 | detail = $detail; 13 | Logger::Error($this->getMessage() . PHP_EOL . $this->getTraceAsString(), $detail); 14 | } 15 | 16 | public function getDetail() 17 | { 18 | return $this->detail; 19 | } 20 | } 21 | 22 | ?> 23 | -------------------------------------------------------------------------------- /sources/app/lib/keyscounter.php: -------------------------------------------------------------------------------- 1 | data[$key] = $value; 13 | } 14 | 15 | public function Add($key) 16 | { 17 | $this->Set($key, $this->Get($key) + 1); 18 | } 19 | 20 | public function Get($key) 21 | { 22 | if(!array_key_exists($key, $this->data)) 23 | { 24 | $this->data[$key] = 0; 25 | } 26 | 27 | return $this->data[$key]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sources/app/views/server-infos.htm: -------------------------------------------------------------------------------- 1 |
2 |

Server infos

3 |
4 |
5 |
Downloading
6 |
{{\App\Models\Downloads::instance()->CountByState(\App\Models\Objects\Download::State_Downloading)}} download(s)
7 |
Max. simultaneous
8 |
{{\App\Models\Config::Get('max_simultaneous')}} download(s)
9 |
Max. concurrents
10 |
{{\App\Models\Config::Get('max_concurrents')}} download(s)
11 |
Free space
12 |
{{\App\Lib\Utils::GetOutputFreeSpace()}}
13 |
14 |
15 |
-------------------------------------------------------------------------------- /sources/app/lib/human.php: -------------------------------------------------------------------------------- 1 | =') 14 | { 15 | $this->version = $version; 16 | $this->operator = $operator; 17 | if(!in_array($operator, ['<', '<=', '>', '>=', '=', '!='])) 18 | { 19 | throw new ComponentException(sprintf('unknown operator %s for validation', $operator)); 20 | } 21 | } 22 | 23 | public function validate($version) 24 | { 25 | return version_compare($version, $this->version, $this->operator); 26 | } 27 | } -------------------------------------------------------------------------------- /sources/app/dict/settings/f3.ini: -------------------------------------------------------------------------------- 1 | ; === Globals === 2 | [globals] 3 | ENCODING = UTF-8 4 | PREFIX = LANG. 5 | ;LANGUAGE = fr,en 6 | FALLBACK = en 7 | 8 | ; === Routes == 9 | ; Syntax: VERBS @alias = callback, ttl, kbps 10 | [routes] 11 | GET @logout: /signout = App\Controllers\Sign->SignOut 12 | GET|POST @signin: /signin = App\Controllers\Sign->SignIn 13 | 14 | ; === Maps === 15 | ; Syntax: @alias: route = class, ttl, kbps 16 | [maps] 17 | @admin: /admin = App\Controllers\Admin 18 | @downloads: /media/@filter = App\Controllers\Downloads 19 | @home: / = App\Controllers\Home 20 | @install: /install = App\Controllers\Install 21 | @links: /links = App\Controllers\Links 22 | @link: /link/@media_id = App\Controllers\Link 23 | 24 | ; === Redirects === 25 | ;[redirects] 26 | 27 | ; === Configs === 28 | [configs] 29 | {{@DIR_SETTINGS}}app.ini=false 30 | {{@DIR_SETTINGS}}console.ini=false -------------------------------------------------------------------------------- /sources/app/controllers/sign.php: -------------------------------------------------------------------------------- 1 | reroute('@home'); 12 | } 13 | 14 | if($f3->get('VERB') == 'POST') 15 | { 16 | $post = $f3->get('POST'); 17 | 18 | try { 19 | \App\Lib\SignedInUser::SignIn($post['login'], $post['password']); 20 | $f3->reroute('@home'); 21 | } 22 | catch(\App\Lib\Exception $e) 23 | { 24 | $f3->set('View.LoginFailed', true); 25 | } 26 | } 27 | 28 | echo \Template::instance()->render('login.html'); 29 | } 30 | 31 | public function SignOut(\Base $f3, array $routes) 32 | { 33 | if(!\App\Lib\SignedInUser::IsUserSignedIn()) 34 | { 35 | $f3->reroute('@home'); 36 | } 37 | 38 | \App\Lib\SignedInUser::SignOut(); 39 | $f3->reroute('@home'); 40 | } 41 | } -------------------------------------------------------------------------------- /sources/app/bin/downloadsmanager.php: -------------------------------------------------------------------------------- 1 | argument() 12 | ->require() 13 | ->referToAs('download') 14 | ->describedAs('Select a download using its id'); 15 | 16 | $commando->option('s') 17 | ->aka('state') 18 | ->describedAs('Set state of selected download'); 19 | 20 | $commando->option('i') 21 | ->aka('do_not_update') 22 | ->boolean() 23 | ->describedAs('Set state of selected download'); 24 | 25 | if(is_null($download = \App\Models\Downloads::instance()->GetById($commando[1]))) 26 | { 27 | throw new Ex_UnknownDownload; 28 | } 29 | 30 | if($commando->hasOption('state')) 31 | { 32 | $download->SetState(intval($commando['state'])); 33 | } 34 | 35 | if(!$commando['do_not_update']) 36 | { 37 | \App\Lib\DownloadsManager::Update(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /sources/app/lib/signedinuser.php: -------------------------------------------------------------------------------- 1 | GetByName($login))) 15 | { 16 | throw new Exception('Unable to sign in: user "'.$login.'" not found.'); 17 | } 18 | 19 | if(!password_verify($password, $user->password)) 20 | { 21 | throw new Exception('Unable to sign in: wrong password for user "'.$login.'".'); 22 | } 23 | 24 | Session::instance()->Set(self::$session_key, $user->_id); 25 | } 26 | 27 | public static function SignOut() 28 | { 29 | if(self::IsUserSignedIn()) 30 | { 31 | Session::instance()->Destroy(); 32 | } 33 | } 34 | 35 | public static function IsUserSignedIn() 36 | { 37 | return Session::instance()->Exists(self::$session_key); 38 | } 39 | } 40 | 41 | ?> 42 | -------------------------------------------------------------------------------- /sources/app/controllers/link.php: -------------------------------------------------------------------------------- 1 | media = \App\Models\Media::instance()->GetById($routes['media_id']))) 12 | { 13 | $f3->error(404); 14 | } 15 | } 16 | 17 | public function Get(\Base $f3, array $routes) 18 | { 19 | $no_info = null; 20 | try { 21 | $this->media->GetFilename(); 22 | } 23 | catch(\App\Models\Objects\Ex_NoInfo $e) 24 | { 25 | $no_info = $e->getDetail(); 26 | } 27 | 28 | $f3->set('View.Media', $this->media); 29 | $f3->set('View.NoInfo', $no_info); 30 | 31 | echo \Template::instance()->render('link.html'); 32 | } 33 | 34 | 35 | public function Post(\Base $f3, array $routes) 36 | { 37 | if($f3->exists('POST.format_id', $format_id)) 38 | { 39 | if(!is_null($this->media->Download((int)$format_id))) 40 | { 41 | echo json_encode(true); 42 | return; 43 | } 44 | } 45 | echo json_encode(false); 46 | } 47 | } -------------------------------------------------------------------------------- /sources/app/lib/command.php: -------------------------------------------------------------------------------- 1 | command = $command; 14 | $this->log_file_path = $log_file_path; 15 | $this->error_file_path = $error_file_path; 16 | } 17 | 18 | public function GetCommand(string $default_log_file_path = null, string $default_error_file_path = null) 19 | { 20 | $command = $this->command; 21 | 22 | $log_file_path = is_null($this->log_file_path) ? $default_log_file_path : $this->log_file_path; 23 | $error_file_path = is_null($this->error_file_path) ? $default_error_file_path : $this->error_file_path; 24 | 25 | return 26 | $this->command . 27 | (is_null($log_file_path) ? '' : ' > ' . escapeshellarg($log_file_path)) . 28 | (is_null($error_file_path) ? '' : ($default_error_file_path == '&1' ? ' 2>&1' : ' 2> ' . escapeshellarg($error_file_path)) 29 | ); 30 | } 31 | } 32 | 33 | ?> -------------------------------------------------------------------------------- /sources/app/lib/session.php: -------------------------------------------------------------------------------- 1 | session = new \DB\Jig\Session(\App\Models\Model::GetJIGDB()); 15 | } 16 | 17 | public function Get($name) 18 | { 19 | return \Base::instance()->get('SESSION.'.$name); 20 | } 21 | 22 | public function Set($name, $value) 23 | { 24 | return \Base::instance()->set('SESSION.'.$name, $value); 25 | } 26 | 27 | public function Push($name, $value) 28 | { 29 | return \Base::instance()->push('SESSION.'.$name, $value); 30 | } 31 | 32 | public function Clear($name) 33 | { 34 | return \Base::instance()->clear('SESSION.'.$name); 35 | } 36 | 37 | public function Exists($name) 38 | { 39 | return \Base::instance()->exists('SESSION.'.$name); 40 | } 41 | 42 | public function Destroy() 43 | { 44 | return \Base::instance()->clear('SESSION'); 45 | } 46 | } -------------------------------------------------------------------------------- /sources/app/dict/l10n/en.php: -------------------------------------------------------------------------------- 1 | array( 7 | \App\Models\Objects\Download::State_Pending => 'Pending', 8 | \App\Models\Objects\Download::State_Downloading => 'Downloading', 9 | \App\Models\Objects\Download::State_Finished => 'Finished', 10 | \App\Models\Objects\Download::State_Error => 'Error', 11 | \App\Models\Objects\Download::State_Paused => 'Paused', 12 | ), 13 | 'Error' => 'Error', 14 | 'LoginFailed' => 'Invalid login/password combination', 15 | 'Login' => 'Login', 16 | 'Password' => 'Password', 17 | 'SignIn' => 'Sign in', 18 | 'FormatQuality' => [ 19 | YoutubeDl::QUALITY_BEST => 'Best', 20 | YoutubeDl::QUALITY_BEST_EVER => 'Best ever', 21 | YoutubeDl::QUALITY_WORST => 'Worst', 22 | YoutubeDl::QUALITY_MANUAL => 'Manual' 23 | ], 24 | 'FormatStream' => [ 25 | YoutubeDl::STREAM_BOTH => 'Audio and Video', 26 | YoutubeDl::STREAM_AUDIO => 'Audio only', 27 | YoutubeDl::STREAM_VIDEO => 'Video only' 28 | ], 29 | 'DownloadFilters' => [ 30 | \App\Controllers\Downloads::Filter_all => 'All', 31 | \App\Controllers\Downloads::Filter_active => 'Active only', 32 | \App\Controllers\Downloads::Filter_inactive => 'Inactive only' 33 | ] 34 | ); -------------------------------------------------------------------------------- /sources/app/lib/install.php: -------------------------------------------------------------------------------- 1 | = 0 && $config['debug_level'] <= 4 ? $config['debug_level'] : 0; 22 | 23 | // URI settings 24 | $config['url_base'] = $config['url_base'] ? 25 | rtrim($config['url_base'], '/') : 26 | $f3->get('SCHEME') . '://' . $f3->get('HOST') . 27 | ( 28 | in_array($f3->get('PORT'), array(80, 443)) ? 29 | '' : 30 | ':' . $f3->get('PORT') 31 | ) . $f3->get('BASE'); 32 | 33 | $this->configurations = $config; 34 | } 35 | 36 | public function GetAll() 37 | { 38 | return $this->configurations; 39 | } 40 | 41 | public function __get($name) 42 | { 43 | return array_key_exists($name, $this->configurations) ? $this->configurations[$name] : null; 44 | } 45 | 46 | public function __isset($name) 47 | { 48 | return isset($this->configurations[$name]); 49 | } 50 | } 51 | 52 | ?> -------------------------------------------------------------------------------- /sources/app/lib/alerter.php: -------------------------------------------------------------------------------- 1 | mutex(__CLASS__, function($message, $title, $type, $object) 16 | { 17 | Session::instance()->Push(self::$session_key, array( 18 | 'type' => $type, 19 | 'title' => $title, 20 | 'message' => $message, 21 | 'object' => $object)); 22 | }, array($message, $title, $type, $object)); 23 | } 24 | 25 | public static function Warning($message, string $title = null, $object=null) 26 | { 27 | self::addMessage($message, self::Level_Warning, $title, $object); 28 | } 29 | 30 | public static function Error($message, string $title = null, $object=null) 31 | { 32 | self::addMessage($message, self::Level_Error, $title, $object); 33 | } 34 | 35 | public static function GetMessages() 36 | { 37 | return \Base::instance()->mutex(__CLASS__, function() { 38 | $messages = Session::instance()->Get(self::$session_key); 39 | Session::instance()->Clear(self::$session_key); 40 | return $messages; 41 | }); 42 | } 43 | } 44 | 45 | ?> 46 | -------------------------------------------------------------------------------- /sources/app/lib/logger.php: -------------------------------------------------------------------------------- 1 | log = new \Log('mediadownloader.log'); 21 | $this->titles = array_flip(\Base::instance()->constants($this, 'TYPE_')); 22 | } 23 | 24 | public function log(int $type, string $message, $detail = null) 25 | { 26 | if(Config::Get('debug_level') >= $type) 27 | { 28 | $this->log->write($this->titles[$type] . ' - ' . $message . (is_null($detail) ? '' : PHP_EOL . var_export($detail, true))); 29 | } 30 | } 31 | 32 | public static function Warning(string $message, $detail = null) 33 | { 34 | self::instance()->log(self::TYPE_WARN, $message, $detail); 35 | } 36 | 37 | public static function Info(string $message, $detail = null) 38 | { 39 | self::instance()->log(self::TYPE_INFO, $message, $detail); 40 | } 41 | 42 | public static function Error(string $message, $detail = null) 43 | { 44 | self::instance()->log(self::TYPE_ERROR, $message, $detail); 45 | } 46 | 47 | public static function Debug(string $message, $detail = null) 48 | { 49 | self::instance()->log(self::TYPE_DEBUG, $message, $detail); 50 | } 51 | } 52 | 53 | ?> 54 | -------------------------------------------------------------------------------- /sources/app/views/links-browse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | < Add new links 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | Go to media list > 30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /sources/app/lib/debugger.php: -------------------------------------------------------------------------------- 1 | debugger = new \DebugBar\DebugBar; 17 | $this->debugger->addCollector(new \DebugBar\DataCollector\PhpInfoCollector); 18 | $this->debugger->addCollector(new \DebugBar\DataCollector\TimeDataCollector($request_start_time)); 19 | $this->debugger->addCollector(new \DebugBar\DataCollector\MemoryCollector); 20 | $this->debugger->addCollector(new \DebugBar\DataCollector\ExceptionsCollector); 21 | $this->debugger->addCollector(new \DebugBar\DataCollector\ConfigCollector(Config::instance()->Dump())); 22 | 23 | $this->renderer = $this->debugger->getJavascriptRenderer($f3->get('URL_BASE') . '/' . \App\SUBDIR_PUBLIC_DEBUGBAR); 24 | } 25 | } 26 | 27 | public function RenderHead() 28 | { 29 | return is_null($this->renderer) ? null : $this->renderer->renderHead(); 30 | } 31 | 32 | public function Render() 33 | { 34 | // Collects F3's hive juste before rendering 35 | !is_null($this->debugger) && $this->debugger->addCollector(new \DebugBar\DataCollector\ConfigCollector(\Base::instance()->hive(), 'F3->Hive')); 36 | 37 | return is_null($this->renderer) ? null : $this->renderer->render(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sources/app/models/media.php: -------------------------------------------------------------------------------- 1 | mapper->beforeupdate(function(\DB\Jig\Mapper $self, array $pkeys) { 14 | self::CheckValues($self); 15 | self::CheckUnique($self); 16 | }); 17 | 18 | $this->mapper->beforeinsert(function(\DB\Jig\Mapper $self, array $pkeys) { 19 | self::CheckValues($self); 20 | self::CheckUnique($self); 21 | }); 22 | } 23 | 24 | public function CheckValues(\DB\Jig\Mapper $self) 25 | { 26 | // Check URL validity 27 | if(filter_var($self->get('url'), FILTER_VALIDATE_URL) === false) 28 | { 29 | throw new Ex_InvalidURL($self->get('url')); 30 | } 31 | } 32 | 33 | public function CheckUnique(\DB\Jig\Mapper $self) 34 | { 35 | if($self->findone(['@_id = ? AND @url = ?', $self->get('_id'), $self->get('url')]) !== false) 36 | { 37 | throw new Ex_Duplicate; 38 | } 39 | } 40 | 41 | public function GetById(string $id) 42 | { 43 | return $this->NewObject($this->mapper->findone(['@_id = ?', $id])); 44 | } 45 | 46 | public function GetByUrl(string $url) 47 | { 48 | return $this->NewObject($this->mapper->findone(['@url = ?', $url])); 49 | } 50 | 51 | public function New(string $url) 52 | { 53 | $this->mapper->reset(); 54 | $this->mapper->url = $url; 55 | $this->mapper->save(); 56 | 57 | return $this->NewObject($this->mapper); 58 | } 59 | } -------------------------------------------------------------------------------- /sources/app/models/users.php: -------------------------------------------------------------------------------- 1 | mapper->beforeupdate(function(\DB\Jig\Mapper $self, array $pkeys) { 16 | self::CheckValues($self); 17 | self::CheckUnique($self); 18 | }); 19 | 20 | $this->mapper->beforeinsert(function(\DB\Jig\Mapper $self, array $pkeys) { 21 | self::CheckValues($self); 22 | self::CheckUnique($self); 23 | }); 24 | 25 | self::$validator = Validator::key('name', Validator::stringType()->length(1)) 26 | ->key('password', Validator::stringType()->length(6)) 27 | ; 28 | } 29 | 30 | public function CheckValues(\DB\Jig\Mapper $self) 31 | { 32 | self::$validator->assert($self->cast()); 33 | 34 | $self->set('password', password_hash($self->get('password'), PASSWORD_DEFAULT)); 35 | } 36 | 37 | public function CheckUnique(\DB\Jig\Mapper $self) 38 | { 39 | if($self->findone(['@name = ?', $self->get('name')]) !== false) 40 | { 41 | throw new Ex_Duplicate($self->get('name')); 42 | } 43 | } 44 | 45 | public function GetByName(string $name) 46 | { 47 | return $this->NewObject($this->mapper->findone(['@name = ?', $name])); 48 | } 49 | 50 | public function New(string $name, string $password) 51 | { 52 | $this->mapper->reset(); 53 | $this->mapper->name = $name; 54 | $this->mapper->password = $password; 55 | $this->mapper->save(); 56 | 57 | return $this->NewObject($this->mapper); 58 | } 59 | } -------------------------------------------------------------------------------- /sources/app/views/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
7 |
8 |
9 |

{{ @LANG.SignIn }}

10 |
11 |
12 | 13 | 18 | 19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 | 46 | -------------------------------------------------------------------------------- /sources/app/views/links-download.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Download request summary

6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | 29 | 30 | 31 | 32 | 33 |
ThumbnailURLStatus
19 | 20 | 21 |
No image
22 |
23 | 24 | {{@View.media->extractor_key}} 25 | 26 |
27 |
{{ @result.url }}{{ is_array(@result.message) ? implode(PHP_EOL, @result.message) : @result.message }}
34 |
35 |
36 |
37 |
38 | < Add new links 39 |
40 |
41 | Go to media list > 42 |
43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /sources/app/controllers/admin.php: -------------------------------------------------------------------------------- 1 | set('View.Config', $f3->get('POST') + Config::instance()->Dump()); 12 | echo \Template::instance()->render('admin.html'); 13 | } 14 | 15 | public function Post(\Base $f3, array $routes) 16 | { 17 | if($f3->exists('POST', $post)) 18 | { 19 | try { 20 | Config::instance()->MSet([ 21 | 'cache' => !empty($post['cache']), 22 | 'debug_level' => intval($post['debug_level']), 23 | 'default_quality' => intval($post['default_quality']), 24 | 'default_stream' => intval($post['default_stream']), 25 | 'download_path' => rtrim($post['download_path'], '/') . '/', 26 | 'max_concurrents' => intval($post['max_concurrents']), 27 | 'max_simultaneous' => intval($post['max_simultaneous']), 28 | 'youtubedl_args' => trim($post['youtubedl_args']), 29 | 'youtubedl_path' => trim($post['youtubedl_path']) 30 | ]); 31 | } 32 | catch(\Respect\Validation\Exceptions\NestedValidationException $e) 33 | { 34 | // Implementation of NestedValidationException::getMessagesIndexedByName (not yet released) 35 | // See: https://github.com/Respect/Validation/pull/773/commits/8b1a7e7079b9471e55ec666f077e927b9f0e8975 36 | $messages = []; 37 | $exceptions = $e->getIterator(); 38 | 39 | foreach($exceptions as $exception) 40 | { 41 | if($exceptions[$exception]['depth'] != 1 && preg_match('/^' . $exception->getName() . '(.*)$/', $exception->getMessage(), $matches)) 42 | { 43 | $messages[$exception->getName()][] = $matches[1]; 44 | } 45 | } 46 | 47 | $f3->set('View.Validation', $messages); 48 | return $this->Get($f3, $routes); 49 | } 50 | } 51 | $f3->reroute(); 52 | } 53 | } -------------------------------------------------------------------------------- /sources/app/lib/validations/exceptions/phpversionexception.php: -------------------------------------------------------------------------------- 1 | [ 18 | self::STANDARD => '{{name}} must be {{operator}} {{version}}.', 19 | self::EQUAL => '{{name}} must be equal to {{version}}.', 20 | self::NOT_EQUAL => '{{name}} must be not equal to {{version}}.', 21 | self::LOWER => '{{name}} must be lower than {{version}}.', 22 | self::LOWER_EQUAL => '{{name}} must be lower than or equal to {{version}}.', 23 | self::GREATER => '{{name}} must be greater than {{operator}} {{version}}.', 24 | self::GREATER_EQUAL => '{{name}} must be greater than or equal to {{version}}.' 25 | ], 26 | self::MODE_NEGATIVE => [ 27 | self::STANDARD => '{{name}} must not be {{operator}} {{version}}.', 28 | self::EQUAL => '{{name}} must not be equal to {{version}}.', 29 | self::NOT_EQUAL => '{{name}} must not be not equal to {{version}}.', 30 | self::LOWER => '{{name}} must not be lower than {{version}}.', 31 | self::LOWER_EQUAL => '{{name}} must not be lower than or equal to {{version}}.', 32 | self::GREATER => '{{name}} must not be greater than {{operator}} {{version}}.', 33 | self::GREATER_EQUAL => '{{name}} must not be greater than or equal to {{version}}.' 34 | ], 35 | ]; 36 | 37 | public function chooseTemplate() 38 | { 39 | switch($this->getParam('operator')) 40 | { 41 | case '=': return static::EQUAL; 42 | case '!=': return static::NOT_EQUAL; 43 | case '<': return static::LOWER; 44 | case '<=': return static::LOWER_EQUAL; 45 | case '>': return static::GREATER; 46 | case '>=': return static::GREATER_EQUAL; 47 | } 48 | return static::STANDARD; 49 | } 50 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Media Downloader 2 | 3 | ## Description 4 | Media Downloader is a light web interface for [Youtube-dl](https://github.com/rg3/youtube-dl). 5 | By hosting your own media downloader, you, your friends and your familly are allowed to use youtube-dl to download music or video on the web without installing anything nor using a terminal. 6 | 7 | A lot of sites are supported (700+), including YouTube, dailymotion, Bandcamp, Facebook, SoundCloud, GoogleDrive, Imgur, Instagram, Vimeo, Vevo, twitch, Twitter... (a full list is available [here](http://rg3.github.io/youtube-dl/supportedsites.html)). 8 | 9 | After the download you can stream your videos from your web browser or save it on your computer directly from the list page. 10 | Media Downloader supports simultaneous downloads in background and now allows you to queuing them! 11 | 12 | ## News 13 | - Queuing downloads 14 | - Integration with FatFree Framework and composer 15 | - Web installer 16 | - New administration page 17 | - New design 18 | - Internal caching system 19 | 20 | ## Requirements 21 | Required: 22 | - Web server 23 | - [Composer](https://getcomposer.org/) 24 | - PHP >= 7 25 | - [Youtube-dl](https://github.com/rg3/youtube-dl) 26 | 27 | Optional: 28 | - avconv or ffmpeg (required for "Best Ever" quality since it will merge both audio and video best available quality) 29 | 30 | ## How to install? 31 | Follow instructions on [dedicated wiki page](https://github.com/Kallys/MediaDownloader/wiki/Installation). 32 | 33 | ## Screenshots 34 | ### Home page 35 | ![Home page screenshot](https://github.com/Kallys/MediaDownloader/raw/dev/public/img/home.jpg) 36 | ### Add links page 37 | ![Links page screenshot](https://github.com/Kallys/MediaDownloader/raw/dev/public/img/links.jpg) 38 | ### Media list page 39 | ![List page screenshot](https://github.com/Kallys/MediaDownloader/raw/dev/public/img/list.jpg) 40 | ### Admin page 41 | ![Admin page screenshot](https://github.com/Kallys/MediaDownloader/raw/dev/public/img/admin.jpg) 42 | 43 | Feel free to fork and contribute if you like this project! 44 | -------------------------------------------------------------------------------- /sources/app/lib/downloadsmanager.php: -------------------------------------------------------------------------------- 1 | mutex(__METHOD__, function() 15 | { 16 | Logger::Debug('DownloadsManager::Update'); 17 | 18 | $stopped_downloads = 0; 19 | $started_downloads = 0; 20 | $running_downloads = 0; 21 | $concurrents_downloads = new KeysCounter; 22 | 23 | // 1) Update and count all downloads in progress 24 | $downloads = \App\Models\Downloads::instance()->GetAllByState(Download::State_Downloading); 25 | 26 | foreach($downloads as $download) 27 | { 28 | // Update download state 29 | $download->Update(); 30 | 31 | // Download is still in progress 32 | if($download->state === Download::State_Downloading) 33 | { 34 | $concurrents_downloads->Add($download->GetMedia()->extractor_key); 35 | $running_downloads++; 36 | } 37 | else 38 | { 39 | $stopped_downloads++; 40 | } 41 | } 42 | 43 | // 2) Download all pending downloads (if possible) 44 | if(Config::Get('max_simultaneous') === 0 || $running_downloads < Config::Get('max_simultaneous')) 45 | { 46 | $downloads = \App\Models\Downloads::instance()->GetAllByState(Download::State_Pending); 47 | 48 | foreach($downloads as $download) 49 | { 50 | // No more slot available 51 | if(Config::Get('max_simultaneous') > 0 && $running_downloads >= Config::Get('max_simultaneous')) 52 | { 53 | break; 54 | } 55 | 56 | $extractor_key = $download->GetMedia()->extractor_key; 57 | 58 | // Concurrent downloads limit reached 59 | if(Config::Get('max_concurrents') > 0 && $concurrents_downloads->Get($extractor_key) >= Config::Get('max_concurrents')) 60 | { 61 | continue; 62 | } 63 | 64 | // Start download 65 | if($download->Download()) 66 | { 67 | $started_downloads++; 68 | $running_downloads++; 69 | $concurrents_downloads->Add($extractor_key); 70 | } 71 | } 72 | } 73 | 74 | return [ 75 | 'running' => $running_downloads, 76 | 'started' => $started_downloads, 77 | 'stopped' => $stopped_downloads 78 | ]; 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sources/app/lib/formatinfo.php: -------------------------------------------------------------------------------- 1 | data = $format_data; 12 | } 13 | 14 | private static function ToText($value, bool $prerequisite = true) 15 | { 16 | return $prerequisite ? (empty($value) ? '?' : $value) : '-'; 17 | } 18 | 19 | public function GetFormatId() 20 | { 21 | return $this->data->format_id; 22 | } 23 | 24 | public function GetUrl() 25 | { 26 | return $this->data->url; 27 | } 28 | 29 | public function GetFormatName() 30 | { 31 | return empty($this->data->format) ? $this->data->format_note : $this->data->format; 32 | } 33 | 34 | public function GetStreamType() 35 | { 36 | if($this->HasVideoStream() && $this->HasAudioStream()) 37 | { 38 | return YoutubeDl::STREAM_BOTH; 39 | } 40 | 41 | return $this->HasVideoStream() ? YoutubeDl::STREAM_VIDEO : YoutubeDl::STREAM_AUDIO; 42 | } 43 | 44 | public function HasAudioStream() 45 | { 46 | return isset($this->data->acodec) && $this->data->acodec != 'none'; 47 | } 48 | 49 | public function HasVideoStream() 50 | { 51 | return isset($this->data->vcodec) && $this->data->vcodec != 'none'; 52 | } 53 | 54 | public function GetResolution() 55 | { 56 | return $this->HasVideoStream() ? self::ToText($this->data->width) . 'x' . self::ToText($this->data->height) : '-'; 57 | } 58 | 59 | public function GetVideoCodec() 60 | { 61 | return self::ToText($this->data->vcodec, $this->HasVideoStream()); 62 | } 63 | 64 | public function GetAudioCodec() 65 | { 66 | return self::ToText($this->data->acodec, $this->HasAudioStream()); 67 | } 68 | 69 | public function GetExtension() 70 | { 71 | return self::ToText($this->data->ext); 72 | } 73 | 74 | public function GetFPS() 75 | { 76 | return self::ToText($this->data->fps, $this->HasVideoStream()); 77 | } 78 | 79 | public function GetTBR() 80 | { 81 | return self::ToText($this->data->tbr, $this->HasVideoStream()); 82 | } 83 | 84 | public function GetABR() 85 | { 86 | return self::ToText($this->data->abr, $this->HasAudioStream()); 87 | } 88 | 89 | public function GetFileSize() 90 | { 91 | if(isset($this->data->filesize) && !empty($this->data->filesize)) 92 | return \App\Lib\Human::GetFileSize($this->data->filesize); 93 | 94 | if(isset($this->data->filesize_approx) && !empty($this->data->filesize_approx)) 95 | return '~' . \App\Lib\Human::GetFileSize($this->data->filesize_approx); 96 | 97 | return '?'; 98 | } 99 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | Media Downloader is a fork of the original project "Youtube-dl WebUI" by p1rox 3 | (https://github.com/p1rox/Youtube-dl-WebUI) released under the following 4 | license: 5 | ================================================================================ 6 | 7 | Copyright (c) 2014 Armand VIGNAT 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to 11 | deal in the Software without restriction, including without limitation the 12 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 13 | sell copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | 27 | ================================================================================ 28 | Every change made on the original project and published as "Media Downloader" 29 | (https://github.com/Kallys/MediaDownloader) are released under the following 30 | license: 31 | ================================================================================ 32 | 33 | GNU General Public License, version 3 (GPLv3) 34 | Copyright (c) 2016-2017 Kallys 35 | 36 | This program is free software: you can redistribute it and/or modify 37 | it under the terms of the GNU General Public License as published by 38 | the Free Software Foundation, either version 3 of the License, or 39 | (at your option) any later version. 40 | 41 | This program is distributed in the hope that it will be useful, 42 | but WITHOUT ANY WARRANTY; without even the implied warranty of 43 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 44 | GNU General Public License for more details. 45 | 46 | You should have received a copy of the GNU General Public License 47 | along with this program. If not, see . 48 | -------------------------------------------------------------------------------- /sources/app/views/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 |

Help

11 |
12 |
13 |
With which sites does it work?
14 |
Here's a list of the supported sites.
15 |
Which languages are supported?
16 |
Currently, only english is supported.
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |

About

26 |
27 |
28 |
Version
29 |
1.0
30 |
Project page
31 |
MediaDownloader
32 |
License
33 |
GPLv3
34 |
Maintainer
35 |
Kallys
36 |
37 |
38 |
39 |
40 |
41 |
42 |

Credits

43 |
44 |
45 |
PHP Framework
46 |
Fat-Free (GPLv3)
47 |
PHP Libraries
48 |
Respect/Validation (BSD)
49 |
nategood/commando (MIT)
50 |
maximebf/debugbar (MIT)
51 |
Front end
52 |
Bootstrap (MIT)
53 |
Bootswatch (MIT)
54 |
Downloader
55 |
youtube-dl (Public domain)
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | -------------------------------------------------------------------------------- /sources/app/models/config.php: -------------------------------------------------------------------------------- 1 | mapper->beforeinsert(function(\DB\Jig\Mapper $self, array $pkeys) { 17 | self::$validator->assert($self->cast()); 18 | }); 19 | 20 | Validator::with('App\\Lib\\Validations\\Rules\\'); 21 | self::$validator = Validator::key('cache', Validator::boolType()) 22 | ->key('debug_level', Validator::intType()->between(0, 3)) 23 | ->key('default_quality', Validator::intType()->in(\Base::instance()->constants(YoutubeDl::class, 'QUALITY_'))) 24 | ->key('default_stream', Validator::intType()->in(\Base::instance()->constants(YoutubeDl::class, 'STREAM_'))) 25 | ->key('download_path', Validator::stringType()->directory()->readable()->writable()) 26 | ->key('max_concurrents', Validator::intType()->between(0)) 27 | ->key('max_simultaneous', Validator::intType()->between(0)) 28 | ->key('youtubedl_args', Validator::stringType()) 29 | ->key('youtubedl_path', Validator::stringType()->file()->readable()->executable()->YoutubeDl()) 30 | ; 31 | 32 | $this->mapper->load(); 33 | } 34 | 35 | public function onCreate() 36 | { 37 | $this->mapper->reset(); 38 | $this->mapper->cache = false; 39 | $this->mapper->debug_level = 3; 40 | $this->mapper->default_quality = YoutubeDl::QUALITY_BEST_EVER; 41 | $this->mapper->default_stream = YoutubeDl::STREAM_BOTH; 42 | $this->mapper->download_path = \App\DIR_PUBLIC_DONWLOADS; 43 | $this->mapper->max_concurrents = 1; 44 | $this->mapper->max_simultaneous = 3; 45 | $this->mapper->youtubedl_args = ''; 46 | $this->mapper->youtubedl_path = '/usr/local/bin/youtube-dl'; 47 | $this->mapper->save(); 48 | } 49 | 50 | // Multi-variable assignment using associative array 51 | public function MSet(array $values) 52 | { 53 | // Validate new values before assignment 54 | self::$validator->assert($values + $this->Dump()); 55 | $this->mapper->copyfrom($values); 56 | $this->mapper->save(); 57 | } 58 | 59 | public function __get(string $name) 60 | { 61 | return $this->__isset($name) ? $this->mapper->get($name) : null; 62 | } 63 | 64 | public function __isset(string $name) 65 | { 66 | return $this->mapper->exists($name); 67 | } 68 | 69 | public static function Get(string $name) 70 | { 71 | return self::instance()->__get($name); 72 | } 73 | 74 | public function Dump() 75 | { 76 | return $this->mapper->cast(); 77 | } 78 | } -------------------------------------------------------------------------------- /sources/app/lib/process.php: -------------------------------------------------------------------------------- 1 | GetCommand('/dev/null', '/dev/null'); 16 | 17 | if(!is_null($command_on_success)) 18 | { 19 | $cmd .= ' && ' . $command_on_success->GetCommand('/dev/null', '/dev/null'); 20 | } 21 | 22 | $cmd .= ') > /dev/null 2> /dev/null &'; 23 | $cmd .= ' echo $!'; // Return PID 24 | 25 | Logger::Info('Process::RunBackground: "' . $cmd . '"'); 26 | 27 | exec($cmd, $output, $result); 28 | 29 | if($result != 0) 30 | { 31 | throw new Ex_CommandFailed($cmd, $output, $result); 32 | } 33 | 34 | return $output[0];// Pid 35 | } 36 | 37 | /** 38 | * @return array output result of the command (line by line). 39 | */ 40 | public static function Run(Command $command) 41 | { 42 | Logger::Info('Process::Run: "' . $command->GetCommand() . '"'); 43 | 44 | exec($command->GetCommand(null, '&1'), $output, $result); 45 | 46 | if($result !== 0) 47 | { 48 | throw new Ex_CommandFailed($command->GetCommand(), $output, $result); 49 | } 50 | 51 | return $output; 52 | } 53 | 54 | /** 55 | * @return bool true if the process is running, false otherwise. 56 | */ 57 | public static function IsRunning(int $pid) 58 | { 59 | return $pid > 0 ? posix_kill(intval($pid), 0) : false; 60 | } 61 | 62 | /** 63 | * @return bool true if the process id is the same as current process, false otherwise. 64 | */ 65 | public static function AmI(int $pid) 66 | { 67 | return $pid > 0 ? getmypid() === $pid : false; 68 | } 69 | 70 | public static function Kill(int $pid) 71 | { 72 | if(!self::IsRunning($pid)) 73 | { 74 | throw new Exception('Trying to kill a non running process (PID=' . $pid . ')'); 75 | } 76 | 77 | if(!posix_kill(intval($pid), 15)) 78 | { 79 | throw new Exception('Unable to kill a process (PID=' . $pid . ')'); 80 | } 81 | } 82 | 83 | public static function SearchPid(array $contains, int $uid = null) 84 | { 85 | $search = ''; 86 | foreach($contains as $string) 87 | { 88 | $search .= ' | grep "' . $string . '"'; 89 | } 90 | 91 | if(is_null($uid)) 92 | { 93 | $uid = posix_getuid(); 94 | } 95 | 96 | // Search process id 97 | $output = self::Run(new Command('ps -u ' . $uid . ' -o pid,cmd' . $search)); 98 | 99 | return !preg_match('/^\s*(\d+)\s+/', $output[0], $matches) ? null : intval($matches[0]); 100 | } 101 | } 102 | 103 | ?> -------------------------------------------------------------------------------- /sources/app/views/header.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Media Downloader 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ \App\Lib\Debugger::instance()->RenderHead() }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 51 | 52 |
53 | 54 | 59 | 60 |
61 | -------------------------------------------------------------------------------- /sources/app/models/downloads.php: -------------------------------------------------------------------------------- 1 | mapper->beforeupdate(function(\DB\Jig\Mapper $self, array $pkeys) { 12 | self::CheckValues($self); 13 | self::CheckUnique($self); 14 | }); 15 | 16 | $this->mapper->beforeinsert(function(\DB\Jig\Mapper $self, array $pkeys) { 17 | self::CheckValues($self); 18 | self::CheckUnique($self); 19 | }); 20 | } 21 | 22 | public function CheckValues(\DB\Jig\Mapper $self) 23 | { 24 | if(is_null($media = Media::instance()->GetById($self->get('media_id')))) 25 | { 26 | throw new \App\Lib\Exception('Unknown media'); 27 | } 28 | 29 | if(is_null($media->GetFormatInfosById($self->get('format_id')))) 30 | { 31 | throw new \App\Lib\Exception('Unknown format'); 32 | } 33 | } 34 | 35 | public function CheckUnique(\DB\Jig\Mapper $self) 36 | { 37 | if($self->findone(['@_id != ? AND @media_id = ? AND @format_id = ?', $self->get('_id'), $self->get('media_id'), $self->get('format_id')]) !== false) 38 | { 39 | throw new Ex_Duplicate($self->get('media_id') . ' & ' . $self->get('format_id')); 40 | } 41 | } 42 | 43 | public function GetAllByMediaId(string $media_id = null) 44 | { 45 | return $this->NewObject($this->mapper->find(['@media_id = ?', $media_id])); 46 | } 47 | 48 | public function GetAllByState(int $state) 49 | { 50 | return $this->NewObject($this->mapper->find(['@state = ?', $state])); 51 | } 52 | 53 | public function GetAllOthersByState(int $state) 54 | { 55 | return $this->NewObject($this->mapper->find(['@state != ?', $state])); 56 | } 57 | 58 | public function CountByState(int $state = null) 59 | { 60 | if(is_null($state)) 61 | { 62 | $result = []; 63 | foreach(\Base::instance()->constants(\App\Models\Objects\Download::class, 'State_') as $state) 64 | { 65 | $result[$state] = $this->mapper->count(['@state = ?', $state]); 66 | } 67 | return $result; 68 | } 69 | else 70 | { 71 | return $this->mapper->count(['@state = ?', $state]); 72 | } 73 | } 74 | 75 | public function GetByFormatId(string $media_id, string $format_id) 76 | { 77 | return $this->NewObject($this->mapper->findone(array('@media_id = ? AND @format_id = ?', $media_id, $format_id))); 78 | } 79 | 80 | public function GetById(string $id) 81 | { 82 | return $this->NewObject($this->mapper->findone(array('@_id = ?', $id))); 83 | } 84 | 85 | public function New(string $media_id, string $format_id, int $process_id = 0, string $output = null, int $state = \App\Models\Objects\Download::State_Pending) 86 | { 87 | $this->mapper->reset(); 88 | $this->mapper->media_id = $media_id; 89 | $this->mapper->format_id = $format_id; 90 | $this->mapper->process_id = $process_id; 91 | $this->mapper->output = $output; 92 | $this->mapper->state = $state; 93 | $this->mapper->save(); 94 | 95 | return $this->NewObject($this->mapper); 96 | } 97 | } -------------------------------------------------------------------------------- /sources/app/views/links-add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Add links

6 |
7 |
8 |
9 |
10 |
11 | Video links 12 | 13 |
14 |
15 |
16 |
17 | Options 18 | 19 |
20 | 21 | 26 |
27 | 28 |
29 | 30 | 35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 |

Help

54 |
55 |
56 |
How does it work ?
57 |
58 |
    59 |
  1. Paste your video link(s) in the text area
  2. 60 |
  3. Adjust options
  4. 61 |
  5. Click on then button
  6. 62 |
63 |
64 |
How can I download a video directly to my computer?
65 |
66 |
    67 |
  1. Paste your video link(s) in the text area
  2. 68 |
  3. Set "quality" option to "Manual"
  4. 69 |
  5. Click on the button
  6. 70 |
  7. Click on the button on the wanted format line
  8. 71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /sources/app/models/model.php: -------------------------------------------------------------------------------- 1 | mapper = new \DB\Jig\Mapper(self::GetJIGDB(), $this->GetTableName()); 18 | $this->object_class = $object_class; 19 | 20 | if(!$this->TableExists()) 21 | { 22 | $this->onCreate(); 23 | } 24 | } 25 | 26 | public static function Destroy() 27 | { 28 | if(self::TableExists()) 29 | { 30 | unlink(self::getTablePath()); 31 | } 32 | } 33 | 34 | // Called on first time table is create 35 | protected function onCreate() 36 | { 37 | } 38 | 39 | protected static function CheckBounds(int $value, int $min = null, int $max = null) 40 | { 41 | if(!is_null($min) && $value < $min) 42 | { 43 | throw new Ex_OutOfBounds($value . ' < ' . $min); 44 | } 45 | 46 | if(!is_null($max) && $value > $max) 47 | { 48 | throw new Ex_OutOfBounds($value . ' > ' . $max); 49 | } 50 | } 51 | 52 | // Lazzy DB connection : only connects if needed 53 | public static function GetJIGDB() 54 | { 55 | if(is_null(self::$db)) 56 | { 57 | self::$db = new \DB\Jig(\App\DIR_DATABASES); 58 | } 59 | 60 | return self::$db; 61 | } 62 | 63 | 64 | private static function GetTableName() 65 | { 66 | return (new \ReflectionClass(static::class))->getShortName(); 67 | } 68 | 69 | private static function getTablePath() 70 | { 71 | return \App\DIR_DATABASES . self::GetTableName(); 72 | } 73 | 74 | protected function getFilter(array $fields) 75 | { 76 | $filter[] = ''; 77 | 78 | foreach($fields as $field => $value) 79 | { 80 | if(is_null($value)) 81 | { 82 | continue; 83 | } 84 | 85 | !empty($filter[0]) && $filter[0] .= ' && '; 86 | 87 | if(is_array($value)) 88 | { 89 | if(!empty($value)) 90 | { 91 | $filter[0] .= '('; 92 | $filter[0] .= '@'.$field.' == '.$value[0]; 93 | for($i=1; isset($value[$i]); $i++) 94 | { 95 | $filter[0] .= ' || @'.$field.' == '.$value[$i]; 96 | } 97 | $filter[0] .= ')'; 98 | } 99 | } 100 | else 101 | { 102 | $filter[0] .= '@'.$field.' == ?'; 103 | $filter[] = $value; 104 | } 105 | } 106 | 107 | if(empty($filter[0])) 108 | return null; 109 | 110 | return $filter; 111 | } 112 | 113 | public function GetAll() 114 | { 115 | return $this->NewObject($this->mapper->find()); 116 | } 117 | 118 | public static function TableExists() 119 | { 120 | return file_exists(self::getTablePath()); 121 | } 122 | 123 | 124 | protected function NewObject($mapper) 125 | { 126 | if(is_array($mapper)) 127 | { 128 | $result = []; 129 | foreach($mapper as $m) 130 | { 131 | $result[] = $this->NewObject($m); 132 | } 133 | return $result; 134 | } 135 | 136 | return $mapper ? new $this->object_class($mapper) : null; 137 | } 138 | } -------------------------------------------------------------------------------- /sources/app/controllers/links.php: -------------------------------------------------------------------------------- 1 | render('links-add.html'); 14 | } 15 | 16 | public function Post(\Base $f3, array $routes) 17 | { 18 | $data = $f3->get('POST'); 19 | 20 | if($f3->exists('POST.urls', $urls)) 21 | { 22 | // Index defined => we're browsing 23 | if($f3->exists('POST.url_index', $index)) 24 | { 25 | return $this->BrowseLinks($f3, $urls, $index); 26 | } 27 | 28 | // Trim and remove duplicates from urls 29 | $urls = array_unique(array_filter(array_map('trim', explode("\r\n", $urls)))); 30 | 31 | if(empty($urls)) 32 | { 33 | Alerter::Error('Any of the following given urls are valid:
- ' . implode('
- ', $urls), 'No valid link!'); 34 | } 35 | else 36 | { 37 | // Need selection page 38 | if($data['quality'] == YoutubeDl::QUALITY_MANUAL) 39 | { 40 | return $this->BrowseLinks($f3, $urls); 41 | } 42 | else 43 | { 44 | return $this->DownloadLinks($f3, $urls, $data['quality'], $data['stream']); 45 | } 46 | } 47 | } 48 | 49 | return $this->Get($f3, $routes); 50 | } 51 | 52 | private function BrowseLinks(\Base $f3, array $urls, int $index = 0) 53 | { 54 | $url = $urls[$index]; 55 | $no_info = null; 56 | 57 | try { 58 | if(is_null($media = Media::instance()->GetByUrl($url))) 59 | { 60 | $media = Media::instance()->New($url); 61 | } 62 | $media->GetFilename();// Force lazzy load infos 63 | } 64 | catch(\App\Models\Ex_InvalidURL $e) 65 | { 66 | $no_info = $e->getMessage(); 67 | } 68 | catch(\App\Models\Objects\Ex_NoInfo $e) 69 | { 70 | $no_info = $e->getDetail(); 71 | } 72 | 73 | $f3->set('View.Urls', $urls); 74 | $f3->set('View.UrlIndex', $index); 75 | $f3->set('View.Media', $media); 76 | $f3->set('View.NoInfo', $no_info); 77 | 78 | echo \Template::instance()->render('links-browse.html'); 79 | } 80 | 81 | private function DownloadLinks(\Base $f3, array $urls, int $quality, int $stream) 82 | { 83 | $result = []; 84 | 85 | foreach($urls as $url) 86 | { 87 | $no_info = false; 88 | $status = 'success'; 89 | $message = 'Added to downloads'; 90 | 91 | try 92 | { 93 | if(is_null($media = \App\Models\Media::instance()->GetByUrl($url))) 94 | { 95 | $media = \App\Models\Media::instance()->New($url); 96 | } 97 | 98 | $media->Download($media->QueryFormat($quality, $stream)); 99 | } 100 | catch(\App\Models\Ex_Duplicate $e) 101 | { 102 | $status = 'warning'; 103 | $message = 'This format has been already downloaded.'; 104 | } 105 | catch(\App\Models\Objects\Ex_NoInfo $e) 106 | { 107 | $status = 'danger'; 108 | $no_info = true; 109 | $message = $e->getDetail(); 110 | } 111 | catch(\App\Lib\Exception $e) 112 | { 113 | $status = 'danger'; 114 | $message = $e->getMessage(); 115 | } 116 | 117 | $result[] = [ 118 | 'media' => $media, 119 | 'url' => $url, 120 | 'status' => $status, 121 | 'message' => $message, 122 | 'no_info' => $no_info 123 | ]; 124 | } 125 | 126 | $f3->set('View.Result', $result); 127 | 128 | // Update Manager in order to start downloads 129 | \App\Lib\DownloadsManager::Update(); 130 | 131 | echo \Template::instance()->render('links-download.html'); 132 | } 133 | } -------------------------------------------------------------------------------- /sources/app/controllers/downloads.php: -------------------------------------------------------------------------------- 1 | GetAll(); 23 | break; 24 | 25 | case self::Filter_active: 26 | $downloads = \App\Models\Downloads::instance()->GetAllByState(Download::State_Downloading); 27 | break; 28 | 29 | case self::Filter_inactive: 30 | $downloads = \App\Models\Downloads::instance()->GetAllOthersByState(Download::State_Downloading); 31 | break; 32 | 33 | default: 34 | $f3->error(404); 35 | } 36 | 37 | $counts = \App\Models\Downloads::instance()->CountByState(); 38 | 39 | $f3->set('View.DownloadsCount', [ 40 | self::Filter_all => array_sum($counts), 41 | self::Filter_active => $counts[Download::State_Downloading] 42 | ]); 43 | $f3->set('View.DownloadsCount.' . self::Filter_inactive, $f3->get('View.DownloadsCount.' . self::Filter_all) - $f3->get('View.DownloadsCount.' . self::Filter_active)); 44 | $f3->set('View.Downloads', $downloads); 45 | 46 | echo \Template::instance()->render('downloads.html'); 47 | } 48 | 49 | public function Post(\Base $f3, array $routes) 50 | { 51 | if($f3->exists('POST', $post)) 52 | { 53 | // Actions on selected downloads 54 | if(is_array($post['downloads'])) 55 | { 56 | foreach($post['downloads'] as $download_id => $download_action) 57 | { 58 | if($download = \App\Models\Downloads::instance()->GetById($download_id)) 59 | { 60 | try 61 | { 62 | switch($download_action) 63 | { 64 | case 'delete': 65 | $download->Cancel(); 66 | break; 67 | 68 | case 'pause': 69 | $download->Pause(); 70 | break; 71 | 72 | case 'retry': 73 | case 'resume': 74 | $download->Resume(); 75 | break; 76 | 77 | case 'download': 78 | if($download->state == Download::State_Finished) 79 | { 80 | \Web::instance()->send($download->output); 81 | } 82 | break; 83 | 84 | default: 85 | \App\Lib\Alerter::Warning('Unable to perform unknown action "' . $download_action . '" on download.'); 86 | } 87 | } 88 | catch(\App\Lib\Exception $e) 89 | { 90 | \App\Lib\Alerter::Error('Unable to perform action "' . $download_action . '" on download. (' . $e->getMessage() . ')'); 91 | } 92 | } 93 | } 94 | } 95 | else 96 | { 97 | switch($post['action']) 98 | { 99 | case 'pause_all_downloading': 100 | $downloads = \App\Models\Downloads::instance()->GetAllByState(Download::State_Downloading); 101 | 102 | foreach($downloads as $download) 103 | { 104 | $download->Pause(); 105 | } 106 | break; 107 | 108 | case 'remove_all_finished': 109 | $downloads = \App\Models\Downloads::instance()->GetAllByState(Download::State_Finished); 110 | 111 | foreach($downloads as $download) 112 | { 113 | $download->Cancel(); 114 | } 115 | break; 116 | 117 | default: 118 | \App\Lib\Alerter::Warning('Unable to perform unknown action "' . $post['action'] . '"'); 119 | } 120 | } 121 | } 122 | 123 | $f3->reroute('@downloads'); 124 | } 125 | } -------------------------------------------------------------------------------- /sources/app/controllers/install.php: -------------------------------------------------------------------------------- 1 | error(403); 16 | } 17 | } 18 | 19 | public function Get(\Base $f3, array $routes) 20 | { 21 | $this->CheckRequirements($f3); 22 | echo \Template::instance()->render('install.html'); 23 | } 24 | 25 | private function CheckRequirements(\Base $f3) 26 | { 27 | $messages = []; 28 | Validator::with('App\\Lib\\Validations\\Rules\\'); 29 | $values = [ 30 | 'php_version' => PHP_VERSION, 31 | 'mdc' => DIR_BASE . 'mdc', 32 | 'logs' => \App\DIR_LOGS, 33 | 'databases' => \App\DIR_DATABASES, 34 | 'sessions' => \App\DIR_SESSIONS, 35 | 'temp' => \App\DIR_TEMP 36 | ]; 37 | 38 | try 39 | { 40 | Validator::key('php_version', Validator::PhpVersion('7.0.0')) 41 | ->key('mdc', Validator::stringType()->file()->readable()->executable()) 42 | ->key('logs', Validator::stringType()->directory()->readable()->writable()->executable()) 43 | ->key('databases', Validator::stringType()->directory()->readable()->writable()->executable()) 44 | ->key('sessions', Validator::stringType()->directory()->readable()->writable()->executable()) 45 | ->key('temp', Validator::stringType()->directory()->readable()->writable()->executable()) 46 | ->assert($values); 47 | } 48 | catch(\Respect\Validation\Exceptions\NestedValidationException $e) 49 | { 50 | // Implementation of NestedValidationException::getMessagesIndexedByName (not yet released) 51 | // See: https://github.com/Respect/Validation/pull/773/commits/8b1a7e7079b9471e55ec666f077e927b9f0e8975 52 | $exceptions = $e->getIterator(); 53 | 54 | foreach($exceptions as $exception) 55 | { 56 | $messages[$exception->getName()] = $exception->getMessage(); 57 | } 58 | } 59 | 60 | $f3->set('View.Requirements', [ 61 | 'Values' => $values, 62 | 'Validation' => $messages 63 | ]); 64 | return empty($messages); 65 | } 66 | 67 | public function Post(\Base $f3, array $routes) 68 | { 69 | if($f3->exists('POST', $post)) 70 | { 71 | $messages = []; 72 | 73 | try 74 | { 75 | Validator::key('password_confirm', Validator::stringType()->equals($post['password'])) 76 | ->assert($post); 77 | 78 | // Remove possible users table 79 | Users::Destroy(); 80 | 81 | // Create administrator 82 | Users::instance()->New($post['name'], $post['password']); 83 | 84 | // Create configs 85 | Config::instance()->MSet([ 86 | 'download_path' => rtrim($post['download_path'], '/') . '/', 87 | 'youtubedl_path' => $post['youtubedl_path'] 88 | ]); 89 | 90 | // Log in administrator 91 | \App\Lib\SignedInUser::SignIn($post['name'], $post['password']); 92 | 93 | // Reroute to admin 94 | $f3->reroute('@admin'); 95 | } 96 | catch(\Respect\Validation\Exceptions\NestedValidationException $e) 97 | { 98 | // Remove config on error 99 | Config::Destroy(); 100 | 101 | $messages = $e->findMessages([ 102 | 'name' => 'Name must contain at least one caracter', 103 | 'password' => 'Password must contain at least six caracters', 104 | 'password_confirm' => 'Password confirmation is different from password', 105 | 'download_path' => 'Download path is not a writeable folder', 106 | 'youtubedl_path' => 'Youtube-dl path is invalid' 107 | ]); 108 | } 109 | catch(\App\Models\Ex_Duplicate $e) 110 | { 111 | $messages['name'] = 'A user with same name already exists'; 112 | } 113 | 114 | $f3->set('View.Form', [ 115 | 'Values' => $post, 116 | 'Validation' => $messages 117 | ]); 118 | } 119 | 120 | $this->Get($f3, $routes); 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /sources/app/lib/youtubedl.php: -------------------------------------------------------------------------------- 1 | download_path . '%(title)s-%(uploader)s-' . $download->format_id . '.%(ext)s'); 74 | $cmd .= ' --restrict-filenames'; // --restrict-filenames is for specials chars 75 | $cmd .= ' --load-info-json ' . escapeshellarg($download->GetMedia()->GetInfoFilePath()); 76 | $cmd .= ' --format ' . $download->format_id; 77 | $cmd .= ' --newline'; // Needed in order to read downloading infos 78 | $cmd .= ' ' . escapeshellarg(Config::Get('youtubedl_args')); 79 | 80 | $command = new Command($cmd, $download->GetLogFilePath(), $download->GetErrorFilePath()); 81 | 82 | $cmd_on_success = DIR_BASE . 'mdc downloads ' . $download->_id . ' --state ' . Download::State_Finished; 83 | $command_on_success = new Command($cmd_on_success); 84 | 85 | // Run command 86 | Process::RunBackground($command, $command_on_succes); 87 | 88 | // Search for youtubedl subprocess pid 89 | if(is_null($pid = Process::SearchPid(['python ' . Config::Get('youtubedl_path'), $download->media_id, 'format ' . $download->format_id]))) 90 | { 91 | throw new Exception('Unable to retrieve youtube-dl sub-process PID'); 92 | } 93 | 94 | return $pid; 95 | } 96 | 97 | public static function GetInfos(string $url) 98 | { 99 | $cmd = Config::Get('youtubedl_path'); 100 | $cmd .= ' --skip-download'; 101 | $cmd .= ' --no-warnings'; // JSON parsing will fail if warnings are printed 102 | $cmd .= ' --output "%(extractor)s_%(id)s"'; 103 | $cmd .= ' --restrict-filenames'; 104 | $cmd .= ' --dump-json'; 105 | $cmd .= ' ' . escapeshellarg($url); 106 | 107 | $output = Process::Run(new Command($cmd)); 108 | 109 | if(empty($output[0])) 110 | { 111 | throw new \App\Lib\Exception('Can\'t load info for URL: ' . $url); 112 | } 113 | 114 | return $output[0]; 115 | } 116 | 117 | /** 118 | * 119 | * @param Media $media 120 | * @param int $quality 121 | * @param int $stream 122 | * @throws \App\Lib\Exception 123 | * @return string describing format (ex: "158", "151+128"...) 124 | */ 125 | public static function QueryFormat(Media $media, int $quality, int $stream) 126 | { 127 | $cmd = Config::Get('youtubedl_path'); 128 | $cmd .= ' --skip-download'; 129 | $cmd .= ' --no-warnings'; // JSON parsing will fail if warnings are printed 130 | $cmd .= ' --output "%(format_id)s"'; 131 | $cmd .= ' --dump-json'; 132 | $cmd .= ' --load-info-json ' . escapeshellarg($media->GetInfoFilePath()); 133 | $cmd .= ' --format ' . self::GetFormatOption($quality, $stream); 134 | 135 | $output = Process::Run(new Command($cmd)); 136 | 137 | if(empty($output[0]) || empty($content = json_decode($output[0])) || empty($content->_filename)) 138 | { 139 | throw new \App\Lib\Exception('Can\'t query format for URL: ' . $media->url); 140 | } 141 | 142 | return $content->_filename; 143 | } 144 | } -------------------------------------------------------------------------------- /public/css/gfonts.css: -------------------------------------------------------------------------------- 1 | /* vietnamese */ 2 | @font-face { 3 | font-family: 'Source Sans Pro'; 4 | font-style: normal; 5 | font-weight: 300; 6 | src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(../fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGD_j0nMiB9fPhg_k1wdK2h0.woff2) format('woff2'); 7 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 8 | } 9 | /* latin-ext */ 10 | @font-face { 11 | font-family: 'Source Sans Pro'; 12 | font-style: normal; 13 | font-weight: 300; 14 | src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(../fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGDRVvBvQIc1z78c__uoBcyI.woff2) format('woff2'); 15 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 16 | } 17 | /* latin */ 18 | @font-face { 19 | font-family: 'Source Sans Pro'; 20 | font-style: normal; 21 | font-weight: 300; 22 | src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(../fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGOode0-EuMkY--TSyExeINg.woff2) format('woff2'); 23 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 24 | } 25 | /* vietnamese */ 26 | @font-face { 27 | font-family: 'Source Sans Pro'; 28 | font-style: normal; 29 | font-weight: 400; 30 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(../fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNOAHFN6BivSraYkjhveRHY.woff2) format('woff2'); 31 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 32 | } 33 | /* latin-ext */ 34 | @font-face { 35 | font-family: 'Source Sans Pro'; 36 | font-style: normal; 37 | font-weight: 400; 38 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(../fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlC2Q8seG17bfDXYR_jUsrzg.woff2) format('woff2'); 39 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 40 | } 41 | /* latin */ 42 | @font-face { 43 | font-family: 'Source Sans Pro'; 44 | font-style: normal; 45 | font-weight: 400; 46 | src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url(../fonts/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2) format('woff2'); 47 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 48 | } 49 | /* vietnamese */ 50 | @font-face { 51 | font-family: 'Source Sans Pro'; 52 | font-style: normal; 53 | font-weight: 700; 54 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(../fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGDovqjS_dXPZszO_XltPdNg.woff2) format('woff2'); 55 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 56 | } 57 | /* latin-ext */ 58 | @font-face { 59 | font-family: 'Source Sans Pro'; 60 | font-style: normal; 61 | font-weight: 700; 62 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(../fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGFxe-GPfKKFmiXaJ_Q0GFr8.woff2) format('woff2'); 63 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 64 | } 65 | /* latin */ 66 | @font-face { 67 | font-family: 'Source Sans Pro'; 68 | font-style: normal; 69 | font-weight: 700; 70 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(../fonts/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGEo0As1BFRXtCDhS66znb_k.woff2) format('woff2'); 71 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 72 | } 73 | /* vietnamese */ 74 | @font-face { 75 | font-family: 'Source Sans Pro'; 76 | font-style: italic; 77 | font-weight: 400; 78 | src: local('Source Sans Pro Italic'), local('SourceSansPro-It'), url(../fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoM7YHq4FgHI02B8rPccK0FJQ.woff2) format('woff2'); 79 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; 80 | } 81 | /* latin-ext */ 82 | @font-face { 83 | font-family: 'Source Sans Pro'; 84 | font-style: italic; 85 | font-weight: 400; 86 | src: local('Source Sans Pro Italic'), local('SourceSansPro-It'), url(../fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoM40tgx99jmYGv_xzYuwd1rU.woff2) format('woff2'); 87 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 88 | } 89 | /* latin */ 90 | @font-face { 91 | font-family: 'Source Sans Pro'; 92 | font-style: italic; 93 | font-weight: 400; 94 | src: local('Source Sans Pro Italic'), local('SourceSansPro-It'), url(../fonts/sourcesanspro/v9/M2Jd71oPJhLKp0zdtTvoMxgy2Fsj5sj3EzlXpqVXRKo.woff2) format('woff2'); 95 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 96 | } 97 | -------------------------------------------------------------------------------- /sources/app/models/objects/media.php: -------------------------------------------------------------------------------- 1 | InfoExists($name); 26 | } 27 | 28 | public function __get(string $name) 29 | { 30 | if(parent::__isset($name)) 31 | { 32 | return parent::__get($name); 33 | } 34 | 35 | $val = null; 36 | return $this->InfoExists($name, $val) ? $val : null; 37 | } 38 | 39 | // Lazzy load infos ('coz I can be long) 40 | private function GetInfos() 41 | { 42 | if(is_null($this->infos)) 43 | { 44 | $this->loadInfos(); 45 | } 46 | 47 | return $this->infos; 48 | } 49 | 50 | private function InfoExists(string $name, &$val = null) 51 | { 52 | $val = $this->GetInfos()->{$name}; 53 | return property_exists($this->infos, $name); 54 | 55 | } 56 | 57 | public function GetInfoFilePath() 58 | { 59 | return \App\DIR_TEMP . $this->mapper->_id . '.json'; 60 | } 61 | 62 | public function GetDownloads() 63 | { 64 | if(is_null($this->downloads)) 65 | { 66 | $this->downloads = []; 67 | 68 | if(!is_null($downloads = \App\Models\Downloads::instance()->GetAllByMediaId($this->mapper->_id))) 69 | { 70 | foreach($downloads as $download) 71 | { 72 | $this->downloads[$download->format_id] = $download; 73 | } 74 | } 75 | } 76 | 77 | return $this->downloads; 78 | } 79 | 80 | public function QueryFormat(int $quality, int $stream) 81 | { 82 | // Be sure info file exists 83 | if(!file_exists($this->GetInfoFilePath())) 84 | { 85 | $this->loadInfos(); 86 | } 87 | return YoutubeDl::QueryFormat($this, $quality, $stream); 88 | } 89 | 90 | public function Download(string $format_id) 91 | { 92 | return \App\Models\Downloads::instance()->New($this->mapper->_id, $format_id); 93 | } 94 | 95 | public function GetDownloadByFormatId(string $format_id) 96 | { 97 | return array_key_exists($format_id, $this->GetDownloads()) ? $this->downloads[$format_id] : null; 98 | } 99 | 100 | private function loadInfos() 101 | { 102 | // Load media infos from cached file (json) 103 | if(is_readable($this->GetInfoFilePath()) && ($json = file_get_contents($this->GetInfoFilePath())) !== false) 104 | { 105 | $this->infos = $this->ParseJsonInfos($json); 106 | } 107 | else 108 | { 109 | try { 110 | $json = YoutubeDl::GetInfos($this->mapper->url); 111 | } 112 | catch(\App\Lib\Ex_CommandFailed $e) 113 | { 114 | throw new Ex_NoInfo(null, $e->getDetail()); 115 | } 116 | 117 | $this->infos = $this->ParseJsonInfos($json); 118 | 119 | // Write infos to file 120 | if(file_put_contents($this->getInfoFilePath(), $json) === false) 121 | { 122 | throw new \App\Lib\Exception('Unable to write info file for URL: ' . $this->mapper->url); 123 | } 124 | } 125 | 126 | //$this->root_format = new FormatInfo($this->GetInfos()); 127 | 128 | if(!empty($this->infos->formats)) 129 | { 130 | foreach($this->infos->formats as $format) 131 | { 132 | $this->formats[$format->format_id] = new FormatInfo($format); 133 | } 134 | } 135 | } 136 | 137 | private function ParseJsonInfos(string $json_string) 138 | { 139 | if(empty($content = json_decode($json_string))) 140 | { 141 | throw new Ex_NoInfo('Unable to parse JSON infos.'); 142 | } 143 | 144 | return $content; 145 | } 146 | 147 | public function GetTitle() 148 | { 149 | return $this->IsPlaylist() ? 150 | $this->infos->playlist . ' - ' . $this->infos->title . ' (' . $this->infos->playlist_index . '/' . $this->infos->n_entries . ')' : 151 | $this->infos->title; 152 | } 153 | 154 | public function GetFilename() 155 | { 156 | return $this->GetInfos()->_filename; 157 | } 158 | 159 | public function IsPlaylist() 160 | { 161 | return !is_null($this->GetInfos()->playlist); 162 | } 163 | 164 | public function GetFormatInfosById(string $format_id) 165 | { 166 | if(is_int($format_id)) 167 | { 168 | return array_key_exists($format_id, $this->GetFormatsInfos()) ? [$this->formats[$format_id]] : null; 169 | } 170 | else if(preg_match('/(\d+)\+(\d+)/', $format_id, $matches) && array_key_exists($matches[1], $this->GetFormatsInfos()) && array_key_exists($matches[2], $this->GetFormatsInfos())) 171 | { 172 | return [$this->formats[$matches[1]], $this->formats[$matches[2]]]; 173 | } 174 | return null; 175 | } 176 | 177 | public function GetFormatsInfos() 178 | { 179 | $this->GetInfos(); 180 | return $this->formats; 181 | } 182 | } 183 | 184 | ?> 185 | -------------------------------------------------------------------------------- /sources/app/views/install.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Installation

6 |
7 |
8 | 9 |
10 | Requirements 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 |
NameValueStatus
{{@name}}{{@value}} 26 | 27 | {{ is_array(@View.Requirements.Validation[@name]) ? implode('
', @View.Requirements.Validation[@name]) : @View.Requirements.Validation[@name] }}
28 | 29 |
30 |
35 |
36 | 37 |
38 | Administrator 39 |

Administrator is a super user in charge of setting up this server.

40 |
41 | 42 |
43 | 44 | 45 | {{ @View.Form.Validation.name }} 46 | 47 |
48 |
49 |
50 | 51 |
52 | 53 | 54 | {{ @View.Form.Validation.password }} 55 | 56 |
57 |
58 |
59 | 60 |
61 | 62 | 63 | {{ @View.Form.Validation.password_confirm }} 64 | 65 |
66 |
67 |
68 | 69 |
70 | Server settings 71 |
72 | 73 |
74 | 75 | Path to youtube-dl binary. 76 | 77 | {{ @View.Form.Validation.youtubedl_path }} 78 | 79 |
80 |
81 |
82 | 83 |
84 | 85 | Path on this server where media will be downloaded to. 86 | 87 | {{ @View.Form.Validation.download_path }} 88 | 89 |
90 |
91 |
92 | 93 |
94 | 95 | 96 |
97 | 98 |
99 |
100 |
101 | 102 | -------------------------------------------------------------------------------- /sources/app/app.php: -------------------------------------------------------------------------------- 1 | request_start_time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(true); 66 | 67 | if($this->cli = PHP_SAPI == 'cli') 68 | { 69 | // Command must be instantiate before FatFree (because FatFree emulates HTTP request on PHP_SAPI CLI mode) 70 | $this->commando = new \Commando\Command; 71 | } 72 | 73 | // Load FatFree Framework 74 | $this->f3 = \Base::instance(); 75 | 76 | // Set Fat-Free paths 77 | $this->f3->mset([ 78 | 'AUTOLOAD' => DIR_BASE, 79 | 'UI' => DIR_APP_VIEWS, 80 | 'TEMP' => DIR_TEMP, 81 | 'LOCALES' => DIR_APP_DICT_L10N, 82 | 'LOGS' => DIR_LOGS, 83 | 'UPLOADS' => DIR_TEMP_UPLOADS, 84 | // Custom 85 | 'DIR_SETTINGS' => DIR_APP_DICT_SETTINGS 86 | ] 87 | ); 88 | 89 | // Load settings 90 | $this->f3->config(DIR_APP_DICT_SETTINGS . 'f3.ini', true); 91 | 92 | // Extend hive with URL path 93 | $this->f3->mset([ 94 | 'URL_BASE' => ($this->f3->get('SCHEME') . '://' . $this->f3->get('HOST') . (in_array($this->f3->get('PORT'), [80, 443]) ? '' : ':' . $this->f3->get('PORT')) . $this->f3->get('BASE')), 95 | ]); 96 | 97 | // Load configurations 98 | if(!Config::TableExists()) 99 | { 100 | if($this->f3->get('PATH') !== $this->f3->alias('install')) 101 | { 102 | $this->f3->reroute('@install'); 103 | } 104 | 105 | return; 106 | } 107 | $this->config = Config::instance(); 108 | 109 | // Set Fat-Free dynamic parameters 110 | $this->f3->mset([ 111 | 'CACHE' => $this->config->cache ? 'folder=' . DIR_CACHE : false, 112 | 'DEBUG' => $this->config->debug_level 113 | ] 114 | ); 115 | 116 | // Configure debug mode 117 | if($this->config->debug_level > 2) 118 | { 119 | assert_options(ASSERT_ACTIVE, true); 120 | assert_options(ASSERT_BAIL, true); 121 | assert_options(ASSERT_WARNING, true); 122 | assert_options(ASSERT_QUIET_EVAL, false); 123 | } 124 | } 125 | 126 | public function Run() 127 | { 128 | // Load session if any (must be done before rendering) 129 | \App\Lib\Session::instance(); 130 | 131 | if($this->f3->get('PATH') !== $this->f3->alias('install')) 132 | { 133 | // Configure debug mode 134 | if($this->config->debug_level > 2) 135 | { 136 | // Start debugger 137 | \App\Lib\Debugger::instance($this->f3, $this->request_start_time); 138 | } 139 | 140 | // Authentication is required 141 | if($this->f3->get('PATH') !== $this->f3->alias('signin') && !\App\Lib\SignedInUser::IsUserSignedIn()) 142 | { 143 | $this->f3->reroute('@signin'); 144 | } 145 | } 146 | 147 | return $this->f3->run(); 148 | } 149 | 150 | public function RunConsole() 151 | { 152 | // Define first option 153 | $this->commando->argument() 154 | ->require() 155 | ->referToAs('command') 156 | ->describedAs('The command name'); 157 | 158 | // Do not read access $command or command definition won't work 159 | $command = $this->f3->get('SERVER.argv.1'); 160 | 161 | if($this->f3->exists('Console.Commands.' . strtolower($command), $bin_class)) 162 | { 163 | return $bin_class::Run($this->commando); 164 | } 165 | else if($command != '/') 166 | { 167 | $this->commando->error(new \Exception('Unknown command')); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /sources/app/views/downloads.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 25 | 26 |
27 |

Actions

28 |
29 |
30 |
31 |

Filters

32 | 37 |
38 |
39 |

Actions

40 |
41 | 42 | 43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 |

Media list

51 |
52 | 53 | 54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 84 | 85 | 86 | 87 | 88 | 103 | 104 | 105 | 106 |
ThumbnailHostTitleFormatStateProgressionSpeedETAAction
{{@media->extractor_key}}{{@media->GetTitle()}} 78 | 79 | 80 | 81 | {{@format->GetFormatName()}}
82 |
83 |
{{@LANG.DownloadState[@download->state]}}{{@dlStats.progression}}{{@dlStats.speed}}{{@dlStats.ETA}} 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | 114 | 123 | 124 | -------------------------------------------------------------------------------- /sources/app/models/objects/download.php: -------------------------------------------------------------------------------- 1 | GetErrorFilePath())) 25 | { 26 | return file_get_contents($this->GetErrorFilePath()); 27 | } 28 | 29 | return null; 30 | } 31 | 32 | public function GetLogFileContent() 33 | { 34 | if(is_readable($this->GetLogFilePath())) 35 | { 36 | return file_get_contents($this->GetLogFilePath()); 37 | } 38 | 39 | return null; 40 | } 41 | 42 | public function GetDownloadingStats() 43 | { 44 | $result = array( 45 | 'progression' => '?', 46 | 'speed' => '-', 47 | 'ETA' => '-' 48 | ); 49 | 50 | switch($this->mapper->state) 51 | { 52 | case self::State_Finished: 53 | $result['progression'] = '100%'; 54 | break; 55 | 56 | case self::State_Pending: 57 | case self::State_Downloading: 58 | case self::State_Paused: 59 | case self::State_Error: 60 | if(empty($this->mapper->output) || !is_file($this->mapper->output . '.part')) 61 | { 62 | $result['progression'] = '0.0%'; 63 | } 64 | 65 | if(is_readable($this->GetLogFilePath())) 66 | { 67 | $logs = file($this->GetLogFilePath()); 68 | 69 | foreach(array_reverse($logs) as $log) 70 | { 71 | if(preg_match("/^\[download\]\s+([0-9]{1,3}\.[0-9]{1,2}%) of\s+.* at\s+(.*) ETA\s+(.*)$/", $log, $matches)) 72 | { 73 | $result['progression'] = $matches[1]; 74 | 75 | if($this->mapper->state === self::State_Downloading) 76 | { 77 | $result['speed'] = $matches[2]; 78 | $result['ETA'] = $matches[3]; 79 | } 80 | break; 81 | } 82 | } 83 | } 84 | break; 85 | } 86 | 87 | return $result; 88 | } 89 | 90 | public function GetMedia() 91 | { 92 | if(is_null($this->media)) 93 | { 94 | $this->media = \App\Models\Media::instance()->GetById($this->mapper->media_id); 95 | } 96 | 97 | return $this->media; 98 | } 99 | 100 | public function GetFormat() 101 | { 102 | return $this->GetMedia()->GetFormatInfosById($this->mapper->format_id); 103 | } 104 | 105 | public function GetLogFilePath() 106 | { 107 | return \App\DIR_TEMP . $this->mapper->media_id . '.' . $this->mapper->format_id . '.log'; 108 | } 109 | 110 | public function GetErrorFilePath() 111 | { 112 | return \App\DIR_TEMP . $this->mapper->media_id . '.' . $this->mapper->format_id . '.err'; 113 | } 114 | 115 | public function GetTempOutputFilePath() 116 | { 117 | return empty($this->mapper->output) ? null : $this->mapper->output . '.part'; 118 | } 119 | 120 | private function GetMutexId() 121 | { 122 | return __CLASS__ . $this->mapper->media_id . $this->mapper->format_id; 123 | } 124 | 125 | public function GetHash() 126 | { 127 | return preg_replace("/[^[:alnum:][:space:]]/u", "", $this->mapper->_id); 128 | } 129 | 130 | public function SetState(int $state) 131 | { 132 | if(!in_array($state, \Base::instance()->constants($this, 'State_'))) 133 | { 134 | throw new Ex_UnknownState; 135 | } 136 | 137 | if($state == self::State_Finished) 138 | { 139 | // Remove files 140 | is_file($this->GetErrorFilePath()) && unlink($this->GetErrorFilePath()); 141 | is_file($this->GetLogFilePath()) && unlink($this->GetLogFilePath()); 142 | } 143 | 144 | $this->mapper->state = $state; 145 | $this->mapper->save(); 146 | } 147 | 148 | // Should be called only on downloading downloads 149 | public function Update() 150 | { 151 | if($this->mapper->state !== self::State_Downloading) 152 | { 153 | return; 154 | } 155 | 156 | // Parse output file name if not already done 157 | if(empty($this->mapper->output) && is_readable($this->GetLogFilePath())) 158 | { 159 | $logs = file($this->GetLogFilePath()); 160 | 161 | // Prevent potential conflicts 162 | if(preg_match('/^\[download\]\s+(.*)\s+has already been downloaded and merged$/', $logs[count($logs) - 1], $matches)) 163 | { 164 | $this->mapper->output = $matches[1]; 165 | $this->mapper->save(); 166 | } 167 | // In case of merging, read backward and search for merging log line 168 | else if(preg_match('/(\d+)\+(\d+)/', $this->mapper->format_id, $format_matches)) 169 | { 170 | foreach(array_reverse($logs) as $log) 171 | { 172 | if(preg_match('/^\[ffmpeg\]\s+Merging formats into\s+"(.*)"$/', $log, $matches)) 173 | { 174 | $this->mapper->output = $matches[1]; 175 | $this->mapper->save(); 176 | break; 177 | } 178 | } 179 | } 180 | else 181 | { 182 | foreach($logs as $log) 183 | { 184 | if(preg_match('/^\[download\]\s+Destination:\s+(.*)$/', $log, $matches)) 185 | { 186 | $this->mapper->output = $matches[1]; 187 | $this->mapper->save(); 188 | break; 189 | } 190 | } 191 | } 192 | } 193 | 194 | // Check if process is still running (process id is the same as current running process when downloading process calls on success callback, ie it finished) 195 | if(!empty($this->mapper->output) && (!Process::IsRunning($this->mapper->process_id) || Process::AmI($this->mapper->process_id))) 196 | { 197 | $this->SetState(is_file($this->mapper->output) ? self::State_Finished : self::State_Error); 198 | $this->mapper->save(); 199 | } 200 | } 201 | 202 | public function Download() 203 | { 204 | return \Base::instance()->mutex($this->GetMutexId(), function($download, $cmd) 205 | { 206 | if(in_array($download->mapper->state, [self::State_Downloading, self::State_Finished])) 207 | { 208 | return true; 209 | } 210 | 211 | $process_id = YoutubeDl::Download($this); 212 | $download->mapper->process_id = $process_id; 213 | $download->mapper->state = self::State_Downloading; 214 | 215 | return $download->mapper->save(); 216 | }, [$this, $cmd]); 217 | } 218 | 219 | public function Resume() 220 | { 221 | return \Base::instance()->mutex($this->GetMutexId(), function($download) 222 | { 223 | if(!in_array($download->mapper->state, [self::State_Paused, self::State_Error])) 224 | { 225 | return false; 226 | } 227 | 228 | $download->mapper->process_id = 0; 229 | $download->mapper->state = self::State_Pending; 230 | return $download->mapper->save(); 231 | }, $this); 232 | } 233 | 234 | public function Pause() 235 | { 236 | return \Base::instance()->mutex($this->GetMutexId(), function($download) 237 | { 238 | if(in_array($download->mapper->state, [self::State_Paused, self::State_Error, self::State_Finished])) 239 | { 240 | return true; 241 | } 242 | 243 | if(Process::IsRunning($download->mapper->process_id)) 244 | { 245 | Process::Kill($download->mapper->process_id); 246 | $download->mapper->process_id = 0; 247 | } 248 | 249 | $download->mapper->state = self::State_Paused; 250 | $download->mapper->save(); 251 | }, $this); 252 | } 253 | 254 | public function Cancel() 255 | { 256 | return \Base::instance()->mutex($this->GetMutexId(), function($download) 257 | { 258 | if(Process::IsRunning($download->mapper->process_id)) 259 | { 260 | Process::Kill($download->mapper->process_id); 261 | } 262 | 263 | // Remove files 264 | is_file($download->GetErrorFilePath()) && unlink($download->GetErrorFilePath()); 265 | is_file($download->GetLogFilePath()) && unlink($download->GetLogFilePath()); 266 | 267 | if(!empty($download->mapper->output)) 268 | { 269 | is_file($download->mapper->output) && unlink($download->mapper->output); 270 | is_file($download->GetTempOutputFilePath()) && unlink($download->GetTempOutputFilePath()); 271 | } 272 | 273 | // Remove download from DB 274 | return $download->mapper->erase(); 275 | }, $this); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /sources/app/views/link.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

No media informations

5 |
6 |

Unable to load informations for url:

7 |

{{@View.Urls[@View.UrlIndex]}}

8 |

Common errors to check:

9 |
    10 |
  • typo in url
  • 11 |
  • server has no internet access
  • 12 |
  • youtube-dl does not support this provider
  • 13 |
  • provider is down or blacklisted your server
  • 14 |
15 |
16 |
17 |

Error detail:

18 |

{{is_array(@View.NoInfo) ? implode(PHP_EOL, @View.NoInfo) : @View.NoInfo}}

19 |
20 |
21 |
22 |
23 | 24 | 50 | 51 |
52 |

Select formats to download

53 |
54 |
55 |
56 |
57 | 58 | {{@View.Media->extractor_key}} 59 | 60 |
61 |
62 |
63 |
64 |
Title
65 |
{{@View.Media->GetTitle() ?: '-' }}
66 |
Creator
67 |
{{@View.Media->creator ?: '-' }}
68 |
Uploader
69 |
70 | 71 | {{@View.Media->uploader ?: '-' }} 72 | {{@View.Media->uploader ?: '-' }} 73 | 74 |
75 |
Categories
76 |
{{ is_array(@View.Media->categories) ? implode(', ', @View.Media->categories) : '-' }}
77 |
License
78 |
{{@View.Media->license ?: '-' }}
79 |
80 |
81 |
82 |
83 |
Duration
84 |
{{ is_null(@View.Media->duration) ? '-' : gmdate("H:i:s", @View.Media->duration) }}
85 |
Upload date
86 |
{{ is_null(@View.Media->upload_date) ? '-' : date("Y-m-d", strtotime(@View.Media->upload_date)) }}
87 |
Like count
88 |
{{ is_null(@View.Media->like_count) ? '-' : \App\Lib\Human::GetFactor(@View.Media->like_count) }}
89 |
View count
90 |
{{ is_null(@View.Media->view_count) ? '-' : \App\Lib\Human::GetFactor(@View.Media->view_count) }}
91 |
Dislike count
92 |
{{ is_null(@View.Media->dislike_count) ? '-' : \App\Lib\Human::GetFactor(@View.Media->dislike_count) }}
93 |
Age limit
94 |
{{is_null(@View.Media->age_limit) ? '-' : @View.Media->age_limit}}
95 |
96 |
97 |
98 | 99 |
100 |

Description

101 |

{{@View.Media->description}}

102 |
103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 145 | 146 | 147 | 148 |
TypeFormatExtensionFilesizeResolutionVideo codecAudio codecFPSTBRABRDownload
125 | 126 | 127 | {{@formatInfo->GetFormatName()}}{{@formatInfo->GetExtension()}}{{@formatInfo->GetFileSize()}}{{@formatInfo->GetResolution()}}{{@formatInfo->GetVideoCodec()}}{{@formatInfo->GetAudioCodec()}}{{@formatInfo->GetFPS()}}{{@formatInfo->GetTBR()}}{{@formatInfo->GetABR()}} 138 | 141 | 144 |
149 |
150 |
151 |
152 |
153 | 154 | -------------------------------------------------------------------------------- /sources/app/views/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

Administration

6 |
7 |
8 | 9 |
10 | Download settings 11 |
12 | 13 | 14 | Path on this server where media will be downloaded to. 15 | 16 | Validation failed with theses messages: 17 |
    18 | 19 |
  • {{@value}}
  • 20 |
    21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | Maximal limit of simultaneous downloads. Set to 0 to disable this limit. 29 | 30 | Validation failed with theses messages: 31 |
    32 | 33 |
  • {{@value}}
  • 34 |
    35 |
36 |
37 |
38 |
39 | 40 | 41 | Maximal limit of downloads with same origin. Set to 0 to disable this limit. 42 | 43 | Validation failed with theses messages: 44 |
    45 | 46 |
  • {{@value}}
  • 47 |
    48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 | 60 | Default quality selected value when adding links. 61 | 62 | Validation failed with theses messages: 63 |
    64 | 65 |
  • {{@value}}
  • 66 |
    67 |
68 |
69 |
70 |
71 | 72 | 77 | Default stream selected value when adding links. 78 | 79 | Validation failed with theses messages: 80 |
    81 | 82 |
  • {{@value}}
  • 83 |
    84 |
85 |
86 |
87 |
88 |
89 | 90 |
91 | Youtube-dl settings 92 |
93 | 94 | 95 | Path to youtube-dl binary. 96 | 97 | Validation failed with theses messages: 98 |
    99 | 100 |
  • {{@value}}
  • 101 |
    102 |
103 |
104 |
105 |
106 | 107 | 108 | Extra arguments added to youtube-dl download command. 109 | 110 | Validation failed with theses messages: 111 |
    112 | 113 |
  • {{@value}}
  • 114 |
    115 |
116 |
117 |
118 |
119 | 120 |
121 | App settings 122 |
123 |
124 | 125 | 126 | Verbosity level of debug informations. 127 | 128 | Validation failed with theses messages: 129 |
    130 | 131 |
  • {{@value}}
  • 132 |
    133 |
134 |
135 |
136 |
137 | 141 | 142 | Validation failed with theses messages: 143 |
    144 | 145 |
  • {{@value}}
  • 146 |
    147 |
148 |
149 |
150 |
151 |
152 | 153 |
154 | 155 | 156 | 157 | 158 | 159 | Reset 160 | 161 | 162 | 163 |
164 | 165 |
166 |
167 |
168 | 169 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "1d577393d59219826ffd20d6ec0433ab", 8 | "packages": [ 9 | { 10 | "name": "bcosca/fatfree-core", 11 | "version": "3.6.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/bcosca/fatfree-core.git", 15 | "reference": "ded2875b61e69e980d04346ca9b42350ffad8d70" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/bcosca/fatfree-core/zipball/ded2875b61e69e980d04346ca9b42350ffad8d70", 20 | "reference": "ded2875b61e69e980d04346ca9b42350ffad8d70", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.4" 25 | }, 26 | "type": "library", 27 | "autoload": { 28 | "classmap": [ 29 | "." 30 | ] 31 | }, 32 | "notification-url": "https://packagist.org/downloads/", 33 | "license": [ 34 | "GPL-3.0" 35 | ], 36 | "description": "A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!", 37 | "homepage": "http://fatfreeframework.com/", 38 | "time": "2017-06-26T10:03:43+00:00" 39 | }, 40 | { 41 | "name": "kevinlebrun/colors.php", 42 | "version": "0.4.1", 43 | "source": { 44 | "type": "git", 45 | "url": "https://github.com/kevinlebrun/colors.php.git", 46 | "reference": "d132f36d06e48ea080855af19b4bcb1fb615224a" 47 | }, 48 | "dist": { 49 | "type": "zip", 50 | "url": "https://api.github.com/repos/kevinlebrun/colors.php/zipball/d132f36d06e48ea080855af19b4bcb1fb615224a", 51 | "reference": "d132f36d06e48ea080855af19b4bcb1fb615224a", 52 | "shasum": "" 53 | }, 54 | "require": { 55 | "php": ">=5.3.0" 56 | }, 57 | "require-dev": { 58 | "phpunit/phpunit": "3.7.*", 59 | "satooshi/php-coveralls": "dev-master", 60 | "squizlabs/php_codesniffer": "1.*" 61 | }, 62 | "type": "library", 63 | "autoload": { 64 | "psr-0": { 65 | "Colors": "src/" 66 | } 67 | }, 68 | "notification-url": "https://packagist.org/downloads/", 69 | "license": [ 70 | "MIT" 71 | ], 72 | "authors": [ 73 | { 74 | "name": "Kevin Le Brun", 75 | "email": "lebrun.k@gmail.com", 76 | "homepage": "http://kevinlebrun.fr", 77 | "role": "developer" 78 | } 79 | ], 80 | "description": "Colors for PHP CLI scripts", 81 | "homepage": "https://github.com/kevinlebrun/colors.php", 82 | "keywords": [ 83 | "cli", 84 | "color", 85 | "colors", 86 | "console", 87 | "shell" 88 | ], 89 | "time": "2014-12-23T01:23:37+00:00" 90 | }, 91 | { 92 | "name": "nategood/commando", 93 | "version": "0.2.9", 94 | "source": { 95 | "type": "git", 96 | "url": "https://github.com/nategood/commando.git", 97 | "reference": "ba476a3928dcecfb257f9daf7d965a3abe53b6bd" 98 | }, 99 | "dist": { 100 | "type": "zip", 101 | "url": "https://api.github.com/repos/nategood/commando/zipball/ba476a3928dcecfb257f9daf7d965a3abe53b6bd", 102 | "reference": "ba476a3928dcecfb257f9daf7d965a3abe53b6bd", 103 | "shasum": "" 104 | }, 105 | "require": { 106 | "kevinlebrun/colors.php": "~0.2", 107 | "php": ">=5.3" 108 | }, 109 | "type": "library", 110 | "autoload": { 111 | "psr-0": { 112 | "Commando": "src/" 113 | } 114 | }, 115 | "notification-url": "https://packagist.org/downloads/", 116 | "license": [ 117 | "MIT" 118 | ], 119 | "authors": [ 120 | { 121 | "name": "Nate Good", 122 | "email": "me@nategood.com" 123 | } 124 | ], 125 | "description": "PHP CLI Commando Style", 126 | "homepage": "http://github.com/nategood/commando", 127 | "keywords": [ 128 | "automation", 129 | "cli", 130 | "command", 131 | "command line", 132 | "command line interface", 133 | "scripting" 134 | ], 135 | "time": "2016-11-09T16:22:34+00:00" 136 | }, 137 | { 138 | "name": "respect/validation", 139 | "version": "1.1.12", 140 | "source": { 141 | "type": "git", 142 | "url": "https://github.com/Respect/Validation.git", 143 | "reference": "5ab87d1dd932872f6670136a513f72ff9ea41c67" 144 | }, 145 | "dist": { 146 | "type": "zip", 147 | "url": "https://api.github.com/repos/Respect/Validation/zipball/5ab87d1dd932872f6670136a513f72ff9ea41c67", 148 | "reference": "5ab87d1dd932872f6670136a513f72ff9ea41c67", 149 | "shasum": "" 150 | }, 151 | "require": { 152 | "php": ">=5.4", 153 | "symfony/polyfill-mbstring": "^1.2" 154 | }, 155 | "require-dev": { 156 | "egulias/email-validator": "~1.2", 157 | "malkusch/bav": "~1.0", 158 | "mikey179/vfsstream": "^1.5", 159 | "phpunit/phpunit": "~4.0", 160 | "symfony/validator": "~2.6.9", 161 | "zendframework/zend-validator": "~2.3" 162 | }, 163 | "suggest": { 164 | "egulias/email-validator": "Strict (RFC compliant) email validation", 165 | "ext-bcmath": "Arbitrary Precision Mathematics", 166 | "ext-mbstring": "Multibyte String Functions", 167 | "fabpot/php-cs-fixer": "Fix PSR2 and other coding style issues", 168 | "malkusch/bav": "German bank account validation", 169 | "symfony/validator": "Use Symfony validator through Respect\\Validation", 170 | "zendframework/zend-validator": "Use Zend Framework validator through Respect\\Validation" 171 | }, 172 | "type": "library", 173 | "extra": { 174 | "branch-alias": { 175 | "dev-master": "1.1-dev" 176 | } 177 | }, 178 | "autoload": { 179 | "psr-4": { 180 | "Respect\\Validation\\": "library/" 181 | } 182 | }, 183 | "notification-url": "https://packagist.org/downloads/", 184 | "license": [ 185 | "BSD Style" 186 | ], 187 | "authors": [ 188 | { 189 | "name": "Respect/Validation Contributors", 190 | "homepage": "https://github.com/Respect/Validation/graphs/contributors" 191 | } 192 | ], 193 | "description": "The most awesome validation engine ever created for PHP", 194 | "homepage": "http://respect.github.io/Validation/", 195 | "keywords": [ 196 | "respect", 197 | "validation", 198 | "validator" 199 | ], 200 | "time": "2017-03-14T09:44:11+00:00" 201 | }, 202 | { 203 | "name": "symfony/polyfill-mbstring", 204 | "version": "v1.5.0", 205 | "source": { 206 | "type": "git", 207 | "url": "https://github.com/symfony/polyfill-mbstring.git", 208 | "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" 209 | }, 210 | "dist": { 211 | "type": "zip", 212 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", 213 | "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", 214 | "shasum": "" 215 | }, 216 | "require": { 217 | "php": ">=5.3.3" 218 | }, 219 | "suggest": { 220 | "ext-mbstring": "For best performance" 221 | }, 222 | "type": "library", 223 | "extra": { 224 | "branch-alias": { 225 | "dev-master": "1.5-dev" 226 | } 227 | }, 228 | "autoload": { 229 | "psr-4": { 230 | "Symfony\\Polyfill\\Mbstring\\": "" 231 | }, 232 | "files": [ 233 | "bootstrap.php" 234 | ] 235 | }, 236 | "notification-url": "https://packagist.org/downloads/", 237 | "license": [ 238 | "MIT" 239 | ], 240 | "authors": [ 241 | { 242 | "name": "Nicolas Grekas", 243 | "email": "p@tchwork.com" 244 | }, 245 | { 246 | "name": "Symfony Community", 247 | "homepage": "https://symfony.com/contributors" 248 | } 249 | ], 250 | "description": "Symfony polyfill for the Mbstring extension", 251 | "homepage": "https://symfony.com", 252 | "keywords": [ 253 | "compatibility", 254 | "mbstring", 255 | "polyfill", 256 | "portable", 257 | "shim" 258 | ], 259 | "time": "2017-06-14T15:44:48+00:00" 260 | } 261 | ], 262 | "packages-dev": [ 263 | { 264 | "name": "maximebf/debugbar", 265 | "version": "v1.14.0", 266 | "source": { 267 | "type": "git", 268 | "url": "https://github.com/maximebf/php-debugbar.git", 269 | "reference": "e23a98f2d65607d8aa6c7b409a513f8fdf4acdde" 270 | }, 271 | "dist": { 272 | "type": "zip", 273 | "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/e23a98f2d65607d8aa6c7b409a513f8fdf4acdde", 274 | "reference": "e23a98f2d65607d8aa6c7b409a513f8fdf4acdde", 275 | "shasum": "" 276 | }, 277 | "require": { 278 | "php": ">=5.3.0", 279 | "psr/log": "^1.0", 280 | "symfony/var-dumper": "^2.6|^3.0" 281 | }, 282 | "require-dev": { 283 | "phpunit/phpunit": "^4.0|^5.0" 284 | }, 285 | "suggest": { 286 | "kriswallsmith/assetic": "The best way to manage assets", 287 | "monolog/monolog": "Log using Monolog", 288 | "predis/predis": "Redis storage" 289 | }, 290 | "type": "library", 291 | "extra": { 292 | "branch-alias": { 293 | "dev-master": "1.14-dev" 294 | } 295 | }, 296 | "autoload": { 297 | "psr-4": { 298 | "DebugBar\\": "src/DebugBar/" 299 | } 300 | }, 301 | "notification-url": "https://packagist.org/downloads/", 302 | "license": [ 303 | "MIT" 304 | ], 305 | "authors": [ 306 | { 307 | "name": "Maxime Bouroumeau-Fuseau", 308 | "email": "maxime.bouroumeau@gmail.com", 309 | "homepage": "http://maximebf.com" 310 | }, 311 | { 312 | "name": "Barry vd. Heuvel", 313 | "email": "barryvdh@gmail.com" 314 | } 315 | ], 316 | "description": "Debug bar in the browser for php application", 317 | "homepage": "https://github.com/maximebf/php-debugbar", 318 | "keywords": [ 319 | "debug", 320 | "debugbar" 321 | ], 322 | "time": "2017-08-17T07:17:00+00:00" 323 | }, 324 | { 325 | "name": "psr/log", 326 | "version": "1.0.2", 327 | "source": { 328 | "type": "git", 329 | "url": "https://github.com/php-fig/log.git", 330 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" 331 | }, 332 | "dist": { 333 | "type": "zip", 334 | "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 335 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 336 | "shasum": "" 337 | }, 338 | "require": { 339 | "php": ">=5.3.0" 340 | }, 341 | "type": "library", 342 | "extra": { 343 | "branch-alias": { 344 | "dev-master": "1.0.x-dev" 345 | } 346 | }, 347 | "autoload": { 348 | "psr-4": { 349 | "Psr\\Log\\": "Psr/Log/" 350 | } 351 | }, 352 | "notification-url": "https://packagist.org/downloads/", 353 | "license": [ 354 | "MIT" 355 | ], 356 | "authors": [ 357 | { 358 | "name": "PHP-FIG", 359 | "homepage": "http://www.php-fig.org/" 360 | } 361 | ], 362 | "description": "Common interface for logging libraries", 363 | "homepage": "https://github.com/php-fig/log", 364 | "keywords": [ 365 | "log", 366 | "psr", 367 | "psr-3" 368 | ], 369 | "time": "2016-10-10T12:19:37+00:00" 370 | }, 371 | { 372 | "name": "symfony/var-dumper", 373 | "version": "v3.3.8", 374 | "source": { 375 | "type": "git", 376 | "url": "https://github.com/symfony/var-dumper.git", 377 | "reference": "89fcb5a73e0ede2be2512234c4e40457bb22b35f" 378 | }, 379 | "dist": { 380 | "type": "zip", 381 | "url": "https://api.github.com/repos/symfony/var-dumper/zipball/89fcb5a73e0ede2be2512234c4e40457bb22b35f", 382 | "reference": "89fcb5a73e0ede2be2512234c4e40457bb22b35f", 383 | "shasum": "" 384 | }, 385 | "require": { 386 | "php": "^5.5.9|>=7.0.8", 387 | "symfony/polyfill-mbstring": "~1.0" 388 | }, 389 | "conflict": { 390 | "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" 391 | }, 392 | "require-dev": { 393 | "ext-iconv": "*", 394 | "twig/twig": "~1.34|~2.4" 395 | }, 396 | "suggest": { 397 | "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", 398 | "ext-symfony_debug": "" 399 | }, 400 | "type": "library", 401 | "extra": { 402 | "branch-alias": { 403 | "dev-master": "3.3-dev" 404 | } 405 | }, 406 | "autoload": { 407 | "files": [ 408 | "Resources/functions/dump.php" 409 | ], 410 | "psr-4": { 411 | "Symfony\\Component\\VarDumper\\": "" 412 | }, 413 | "exclude-from-classmap": [ 414 | "/Tests/" 415 | ] 416 | }, 417 | "notification-url": "https://packagist.org/downloads/", 418 | "license": [ 419 | "MIT" 420 | ], 421 | "authors": [ 422 | { 423 | "name": "Nicolas Grekas", 424 | "email": "p@tchwork.com" 425 | }, 426 | { 427 | "name": "Symfony Community", 428 | "homepage": "https://symfony.com/contributors" 429 | } 430 | ], 431 | "description": "Symfony mechanism for exploring and dumping PHP variables", 432 | "homepage": "https://symfony.com", 433 | "keywords": [ 434 | "debug", 435 | "dump" 436 | ], 437 | "time": "2017-08-27T14:52:21+00:00" 438 | }, 439 | { 440 | "name": "thomaspark/bootswatch", 441 | "version": "v3.3.7", 442 | "source": { 443 | "type": "git", 444 | "url": "https://github.com/thomaspark/bootswatch.git", 445 | "reference": "4b0d3762f309326a6d66fa4ea18945a7fc11f8ef" 446 | }, 447 | "dist": { 448 | "type": "zip", 449 | "url": "https://api.github.com/repos/thomaspark/bootswatch/zipball/4b0d3762f309326a6d66fa4ea18945a7fc11f8ef", 450 | "reference": "4b0d3762f309326a6d66fa4ea18945a7fc11f8ef", 451 | "shasum": "" 452 | }, 453 | "suggest": { 454 | "robloach/component-installer": "Allows installation of Components via Composer" 455 | }, 456 | "type": "component", 457 | "extra": { 458 | "component": { 459 | "name": "bootswatch", 460 | "files": [ 461 | "README.md", 462 | "LICENSE", 463 | "amelia/*", 464 | "cerulean/*", 465 | "cosmo/*", 466 | "cyborg/*", 467 | "darkly/*", 468 | "flatly/*", 469 | "fonts/*", 470 | "journal/*", 471 | "lumen/*", 472 | "paper/*", 473 | "readable/*", 474 | "sandstone/*", 475 | "simplex/*", 476 | "slate/*", 477 | "spacelab/*", 478 | "superhero/*", 479 | "united/*", 480 | "yeti/*" 481 | ] 482 | } 483 | }, 484 | "notification-url": "https://packagist.org/downloads/", 485 | "license": [ 486 | "MIT" 487 | ], 488 | "authors": [ 489 | { 490 | "name": "thomaspark", 491 | "email": "thomas@bootswatch.com" 492 | } 493 | ], 494 | "description": "Themes for Bootstrap", 495 | "time": "2016-07-30T21:56:26+00:00" 496 | } 497 | ], 498 | "aliases": [], 499 | "minimum-stability": "stable", 500 | "stability-flags": [], 501 | "prefer-stable": false, 502 | "prefer-lowest": false, 503 | "platform": [], 504 | "platform-dev": [] 505 | } 506 | -------------------------------------------------------------------------------- /public/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} 5 | -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>2)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.6",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.6",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.6",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.6",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.6",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.6",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.6",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.6",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------