├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── youtube-api.php ├── database └── migrations │ ├── 2021_10_06_094035_create_logs_table.php │ └── 2021_10_31_125901_add_user_to_log.php ├── resources └── views │ ├── home │ └── index.blade.php │ ├── logs │ └── index.blade.php │ └── template.blade.php ├── routes ├── api.php └── web.php ├── src ├── Controllers │ ├── ApiController.php │ └── HomeController.php ├── Drivers │ ├── Cobalt.php │ ├── IDriver.php │ └── Local.php ├── Models │ ├── Log.php │ └── Video.php └── YoutubeAPIServiceProvider.php └── tests └── ApiTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: MichaelBelgium 4 | custom: https://www.paypal.com/donate/?hosted_button_id=ZJSKX2ASR3ARL -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | composer.lock 3 | docs 4 | vendor 5 | coverage 6 | .vscode 7 | .idea -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Michael V. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Youtube API 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/michaelbelgium/laravel-youtube-api.svg?style=flat-square)](https://packagist.org/packages/michaelbelgium/laravel-youtube-api) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/michaelbelgium/laravel-youtube-api.svg?style=flat-square)](https://packagist.org/packages/michaelbelgium/laravel-youtube-api) 5 | 6 | This package provides a simple youtube api for your Laravel application. It is based on my non-laravel package [Youtube API](https://github.com/MichaelBelgium/Youtube-to-mp3-API). 7 | 8 | ## Installation 9 | 10 | * Install the package via composer: 11 | 12 | ```bash 13 | composer require michaelbelgium/laravel-youtube-api 14 | ``` 15 | 16 | * Optional: publish the config file and edit if u like: 17 | ```bash 18 | php artisan vendor:publish --tag=youtube-api-config 19 | ``` 20 | 21 | * This package uses [the public disk](https://laravel.com/docs/8.x/filesystem#the-public-disk) of Laravel. Run this command to create a symbolic link to the public folder so that converted Youtube downloads are accessible: 22 | ```bash 23 | php artisan storage:link 24 | ``` 25 | 26 | * Execute package migrations 27 | ``` 28 | php artisan migrate 29 | ``` 30 | 31 | # Software 32 | 33 | Depending on what driver you use - on the server where your laravel app is located, you'll need to install some packages. 34 | 35 | * Install ffmpeg (+ libmp3lame - see [this wiki](https://github.com/MichaelBelgium/Youtube-to-mp3-API/wiki/Installing-"ffmpeg"-and-"libmp3lame"-manually) for tutorial) 36 | * [install youtube-dl](http://ytdl-org.github.io/youtube-dl/download.html) 37 | 38 | # API Usage 39 | 40 | This package adds 3 api routes. The route prefix, `/ytconverter/` in this case, is configurable. 41 | 42 | * POST|GET /ytconverter/convert 43 | * DELETE /ytconverter/{id} 44 | * GET /ytconverter/search 45 | * GET /ytconverter/info 46 | 47 | Check the wiki page of this repository for more information about the routes. 48 | 49 | To enable the search endpoint you need to acquire a Google API key on the [Google Developer Console](https://console.developers.google.com) for the API "Youtube Data API v3". Use this key in the environment variable `GOOGLE_API_KEY` 50 | 51 | # Configuration 52 | 53 | ## Driver 54 | 55 | Downloading Youtube video's is not simple these days. To cover this, you can choose how you want to download the video's by setting the `driver` in the configuration. 56 | 57 | Available drivers: 58 | * local 59 | 60 | The default driver. Requires ffmpeg and yt-dlp or youtube-dl to be installed on the server and it'll download files to the server. Metadata comes from yt-dlp. 61 | 62 | * cobalt 63 | 64 | Requires a self hosted [Cobalt](https://github.com/imputnet/cobalt) (API) instance. It doesn't require any software to be installed on the server and it doesn't download files to the server. 65 | If `GOOGLE_API_KEY` is set, it'll use the Youtube Data API to get metadata, otherwise it'll download the video to get the metadata and thus use storage space instead. 66 | 67 | ## API authorization 68 | 69 | If needed, you can protect the API routes with an authentication guard by setting `auth` in the configuration. 70 | 71 | Example: 72 | ```PHP 73 | 'auth' => 'sanctum', 74 | ``` 75 | 76 | ## API rate limiting 77 | 78 | If needed, you can limit API calls by editing the config setting `ratelimiter`. See [Laravel docs](https://laravel.com/docs/8.x/routing#rate-limiting) for more information or examples. 79 | 80 | Example: 81 | 82 | ```PHP 83 | 'ratelimiter' => function (Request $request) { 84 | return Limit::perMinute(5); 85 | }, 86 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "michaelbelgium/laravel-youtube-api", 3 | "description": "Add a youtube api to your Laravel instance", 4 | "keywords": [ 5 | "michaelbelgium", 6 | "laravel-youtube-api", 7 | "youtube-api", 8 | "laravel", 9 | "youtube" 10 | ], 11 | "homepage": "https://github.com/michaelbelgium/laravel-youtube-api", 12 | "license": "MIT", 13 | "type": "library", 14 | "authors": [ 15 | { 16 | "name": "Michael V.", 17 | "email": "michael@michaelbelgium.me", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "google/apiclient": "^2.4", 23 | "illuminate/support": "^8.0|^9.0|^10.0", 24 | "james-heinrich/getid3": "^1.9", 25 | "norkunas/youtube-dl-php": "^2.0" 26 | }, 27 | "require-dev": { 28 | "orchestra/testbench": "^6.0", 29 | "phpunit/phpunit": "^9.0|^10.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "MichaelBelgium\\YoutubeAPI\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "MichaelBelgium\\YoutubeAPI\\Tests\\": "tests" 39 | } 40 | }, 41 | "config": { 42 | "sort-packages": true 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "MichaelBelgium\\YoutubeAPI\\YoutubeAPIServiceProvider" 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/youtube-api.php: -------------------------------------------------------------------------------- 1 | env('YOUTUBE_API_DRIVER', 'local'), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Cobalt server configuration 19 | |-------------------------------------------------------------------------- 20 | | 21 | | This value is the configuration for the cobalt driver 22 | | url: the url of the cobalt server 23 | | hls: whether to use HLS or not 24 | | auth: "Authorization" header for the cobalt server 25 | | 26 | */ 27 | 'cobalt' => [ 28 | 'url' => null, 29 | 'hls' => false, 30 | 'auth' => env('YOUTUBE_API_COBALT_AUTH'), 31 | ], 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Routing prefix 36 | |-------------------------------------------------------------------------- 37 | | 38 | | This value is the section of all the api routes after 'api/' 39 | | Example: api/ytconverter/convert 40 | | 41 | */ 42 | 43 | 'route_prefix' => 'ytconverter', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Set download length limit 48 | |-------------------------------------------------------------------------- 49 | | 50 | | If not null, sets a limit on the video length that users can convert 51 | | Accepts: 52 | | - null 53 | | - an anonymous function with Illuminate\Http\Request parameter and returning an integer, the max video length in seconds 54 | | 55 | | Examples: 56 | | function (Illuminate\Http\Request $request) 57 | | { 58 | | $plan = $request->user()->getCurrentPlan(); 59 | | 60 | | return $plan->download_limit; 61 | | } 62 | | 63 | | function (Illuminate\Http\Request $request) 64 | | { 65 | | return 300; 66 | | } 67 | */ 68 | 69 | 'videolength_limiter' => null, 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Maximum search results 74 | |-------------------------------------------------------------------------- 75 | | 76 | | Specify the maximum amount of results the search route returns 77 | | 78 | */ 79 | 80 | 'search_max_results' => 10, 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | FFMPEG bin path 85 | |-------------------------------------------------------------------------- 86 | | 87 | | The location of the ffmpeg executable in case when manually build in stead of yum install or apt-get install 88 | | 89 | */ 90 | 91 | 'ffmpeg_path' => null, 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Set authentication 96 | |-------------------------------------------------------------------------- 97 | | 98 | | If not null, it'll attempt to use the defined authentication guard for api routes 99 | | 100 | */ 101 | 102 | 'auth' => null, 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Set ratelimiter 107 | |-------------------------------------------------------------------------- 108 | | 109 | | If not null, sets a ratelimiter on the api group routes 110 | | Accepts: 111 | | - null 112 | | - an anonymous function with Illuminate\Http\Request parameter and returning an instance of Illuminate\Cache\RateLimiting\Limit 113 | | 114 | | More info: https://laravel.com/docs/8.x/routing#rate-limiting 115 | | 116 | */ 117 | 'ratelimiter' => null, 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Enable logging 122 | |-------------------------------------------------------------------------- 123 | | 124 | | Save an entry every time a video gets converted into logs table. 125 | | 126 | | Note: this also enables the page /logs with an overview of all songs that have been converted 127 | | 128 | */ 129 | 'enable_logging' => false, 130 | 131 | /* 132 | |-------------------------------------------------------------------------- 133 | | Proxy Configuration 134 | |-------------------------------------------------------------------------- 135 | | 136 | | If not null, sets a proxy for yt-dlp or youtube-dl. 137 | | Accepts: 138 | | - null 139 | | - a string representing the proxy URL 140 | | 141 | */ 142 | 'proxy' => env('YOUTUBE_API_PROXY'), 143 | ]; -------------------------------------------------------------------------------- /database/migrations/2021_10_06_094035_create_logs_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('youtube_id', 32); 20 | $table->string('title'); 21 | $table->smallInteger('duration')->unsigned(); 22 | $table->string('format', 16)->default('mp3'); 23 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('youtube_logs'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2021_10_31_125901_add_user_to_log.php: -------------------------------------------------------------------------------- 1 | foreignId('user_id')->nullable()->after('id'); 18 | 19 | $table->foreign('user_id')->references('id')->on('users')->onDelete('SET NULL')->onUpdate('CASCADE'); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('youtube_logs', function (Blueprint $table) { 31 | $table->dropConstrainedForeignId('user_id'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /resources/views/home/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('youtube-api-views::template') 2 | 3 | @section('content') 4 |
5 |
6 | 7 |
8 |
9 |
10 |
Convert
11 |
12 |
13 | @if (session('error')) 14 |
{{ session('error') }}
15 | @endif 16 | 17 |
18 | @csrf 19 |
20 | 21 | 22 | @error('url') 23 |
{{ $message }}
24 | @enderror 25 |
26 |
27 | 31 | 32 | @error('format') 33 |
{{ $message }}
34 | @enderror 35 |
36 | @if (config('youtube-api.auth') !== null) 37 |
38 | 39 | 40 |
41 | @endif 42 | 43 |
44 |
45 |
46 | 47 | @if (session('converted')) 48 |
49 |
50 |
Json response
51 |
52 |
53 |
@json(session('converted'), JSON_PRETTY_PRINT)
54 |
55 | 102 |
103 | @endif 104 |
105 | 106 |
107 |
108 |
109 |
Search
110 |
111 |
112 |
113 | @csrf 114 | 115 |
116 | 117 | 118 |
119 | 120 |
121 | 122 | 123 |
124 | 125 | @if (config('youtube-api.auth') !== null) 126 |
127 | 128 | 129 |
130 | @endif 131 | 132 | 133 |
134 |
135 |
136 | 137 | @if (session('searched')) 138 |
139 |
140 |
Json response
141 |
142 |
143 |
@json(session('searched'), JSON_PRETTY_PRINT)
144 |
145 | 178 |
179 | @endif 180 |
181 |
182 |
183 | @endsection -------------------------------------------------------------------------------- /resources/views/logs/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('youtube-api-views::template') 2 | 3 | @section('styles') 4 | 5 | @endsection 6 | 7 | @section('content') 8 |
9 |

Logs

10 | 11 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | @foreach ($logs as $log) 33 | 34 | 35 | 36 | 37 | 38 | 39 | 52 | 53 | @endforeach 54 | 55 |
TimeIDTitleDurationFormatLocation
{{ $log->created_at->format('H:i:s') }}{{ $log->youtube_id }}{{ $log->title }}{{ $log->duration }} seconds{{ $log->format }} 40 | @php 41 | $path = \MichaelBelgium\YoutubeAPI\Models\Video::getDownloadPath($log->youtube_id.".".$log->format); 42 | @endphp 43 | @if (File::exists($path)) 44 | @php 45 | $url = \MichaelBelgium\YoutubeAPI\Models\Video::getDownloadUrl($log->youtube_id.".".$log->format); 46 | @endphp 47 | Converted file 48 | @else 49 | Removed 50 | @endif 51 |
56 | 57 | {{ $logs->links() }} 58 |
59 | @endsection -------------------------------------------------------------------------------- /resources/views/template.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Youtube converter 8 | 9 | 10 | @yield('styles') 11 | 12 | 13 | 30 | 31 | @yield('content') 32 | 33 | 34 | @yield('scripts') 35 | 36 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | name('youtube-api.convert'); 6 | Route::get('/search', [ApiController::class, 'search'])->name('youtube-api.search'); 7 | Route::get('/info', [ApiController::class, 'info'])->name('youtube-api.info'); 8 | Route::delete('/{id}', [ApiController::class, 'delete'])->name('youtube-api.delete'); -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('youtube-api.index'); 7 | Route::post('/', [HomeController::class, 'onPost'])->name('youtube-api.submit'); 8 | Route::get('/logs', [HomeController::class, 'logs'])->name('youtube-api.logs'); -------------------------------------------------------------------------------- /src/Controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | all(), [ 27 | 'url' => ['required', 'string', 'url', 'regex:' . Video::URL_REGEX], 28 | 'format' => [Rule::in(Video::POSSIBLE_FORMATS)] 29 | ]); 30 | 31 | if($validator->fails()) { 32 | return response()->json(['error' => true, 'error_messages' => $validator->errors()], Response::HTTP_BAD_REQUEST); 33 | } 34 | 35 | $validated = $validator->validated(); 36 | 37 | $url = Arr::get($validated, 'url'); 38 | $format = Arr::get($validated, 'format', 'mp3'); 39 | 40 | $lengthLimiter = config('youtube-api.videolength_limiter'); 41 | $selectedDriver = config('youtube-api.driver', 'local'); 42 | 43 | if($selectedDriver == 'local') { 44 | $driver = new Local($url, $format); 45 | } else if ($selectedDriver == 'cobalt') { 46 | $driver = new Cobalt($url, $format); 47 | } else { 48 | return response()->json(['error' => true, 'message' => 'Invalid driver'], Response::HTTP_BAD_REQUEST); 49 | } 50 | 51 | try 52 | { 53 | $video = $driver->convert(); 54 | 55 | if($lengthLimiter !== null && is_callable($lengthLimiter)) 56 | { 57 | $maxLength = $lengthLimiter($request); 58 | 59 | if($video->getDuration() > $maxLength && $maxLength > 0) 60 | throw new Exception("The duration of the video is {$video->getDuration()} seconds while max video length is $maxLength seconds."); 61 | } 62 | 63 | if(config('youtube-api.enable_logging', false) === true) 64 | { 65 | $log = new Log(); 66 | $log->youtube_id = $video->getId(); 67 | $log->title = $video->getTitle(); 68 | $log->duration = $video->getDuration(); 69 | $log->format = $format; 70 | 71 | if(config('youtube-api.auth') !== null) 72 | $log->user()->associate(Auth::user()); 73 | 74 | $log->save(); 75 | } 76 | 77 | return response()->json([ 78 | 'error' => false, 79 | ...$video->toArray() 80 | ]); 81 | } 82 | catch (Exception $e) 83 | { 84 | return response()->json(['error' => true, 'message' => $e->getMessage()], Response::HTTP_BAD_REQUEST); 85 | } 86 | } 87 | 88 | public function delete(Request $request, string $id) 89 | { 90 | $removedFiles = []; 91 | 92 | foreach(Video::POSSIBLE_FORMATS as $format) { 93 | $localFile = Video::getDownloadPath("$id.$format"); 94 | 95 | if(File::exists($localFile)) { 96 | File::delete($localFile); 97 | $removedFiles[] = $format; 98 | } 99 | } 100 | 101 | $resultNotRemoved = array_diff(Video::POSSIBLE_FORMATS, $removedFiles); 102 | 103 | if(empty($removedFiles)) 104 | $message = 'No files removed.'; 105 | else 106 | $message = 'Removed files: ' . implode(', ', $removedFiles) . '.'; 107 | 108 | if(!empty($resultNotRemoved)) 109 | $message .= ' Not removed: ' . implode(', ', $resultNotRemoved); 110 | 111 | return response()->json(['error' => false, 'message' => $message]); 112 | } 113 | 114 | public function info(Request $request) 115 | { 116 | if(empty(env('GOOGLE_API_KEY'))) { 117 | return response()->json(['error' => true, 'message' => 'No google api key specified'], Response::HTTP_BAD_REQUEST); 118 | } 119 | 120 | $query = $request->get('query', $request->get('q')); 121 | 122 | if(empty($query)) { 123 | return response()->json(['error' => true, 'message' => 'No query specified'], Response::HTTP_BAD_REQUEST); 124 | } 125 | 126 | if (filter_var($query, FILTER_VALIDATE_URL)) 127 | $id = Video::getVideoId($query); 128 | else 129 | $id = $query; 130 | 131 | $gClient = new \Google_Client(); 132 | $gClient->setDeveloperKey(env('GOOGLE_API_KEY')); 133 | 134 | $youtube = new \Google_Service_YouTube($gClient); 135 | $response = $youtube->videos->listVideos('snippet,contentDetails', ['id' => $id]); 136 | $ytVideo = $response->getItems()[0] ?? null; 137 | 138 | if ($ytVideo == null) { 139 | return response()->json(['error' => true, 'message' => 'Video not found'], Response::HTTP_NOT_FOUND); 140 | } 141 | 142 | $duration = $ytVideo->getContentDetails()->getDuration(); 143 | $interval = new \DateInterval($duration); 144 | $duration = ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s; 145 | 146 | return response()->json([ 147 | 'error' => false, 148 | 'id' => $id, 149 | 'url' => 'https://youtube.com/watch?v='.$id, 150 | 'channel_url' => 'https://youtube.com/channel/'.$ytVideo->getSnippet()->getChannelId(), 151 | 'channel_id' => $ytVideo->getSnippet()->getChannelId(), 152 | 'channel' => $ytVideo->getSnippet()->getChannelTitle(), 153 | 'title' => $ytVideo->getSnippet()->getTitle(), 154 | 'duration' => $duration, 155 | 'description' => $ytVideo->getSnippet()->getDescription(), 156 | 'published_at' => $ytVideo->getSnippet()->getPublishedAt() 157 | ]); 158 | } 159 | 160 | public function search(Request $request) 161 | { 162 | if(empty(env('GOOGLE_API_KEY'))) { 163 | return response()->json(['error' => true, 'message' => 'No google api key specified'], Response::HTTP_BAD_REQUEST); 164 | } 165 | 166 | $validator = Validator::make($request->all(), [ 167 | 'q' => ['required', 'string'], 168 | 'max_results' => ['numeric'] 169 | ]); 170 | 171 | 172 | if($validator->fails()) { 173 | return response()->json(['error' => true, 'error_messages' => $validator->errors()], Response::HTTP_BAD_REQUEST); 174 | } 175 | 176 | $data = $validator->validated(); 177 | 178 | $q = $data['q']; 179 | $max_results = $data['max_results'] ?? config('youtube-api.search_max_results', 10); 180 | 181 | $gClient = new Google_Client(); 182 | $gClient->setDeveloperKey(env('GOOGLE_API_KEY')); 183 | 184 | $guzzleClient = new Client([ 185 | RequestOptions::HEADERS => [ 186 | 'referer' => env('APP_URL') 187 | ] 188 | ]); 189 | 190 | $gClient->setHttpClient($guzzleClient); 191 | 192 | $ytService = new Google_Service_YouTube($gClient); 193 | 194 | try { 195 | $search = $ytService->search->listSearch('id,snippet', [ 196 | 'q' => $q, 197 | 'maxResults' => $max_results, 198 | 'type' => 'video' 199 | ]); 200 | 201 | $results = []; 202 | 203 | foreach ($search['items'] as $searchResult) 204 | { 205 | $results[] = array( 206 | 'id' => $searchResult['id']['videoId'], 207 | 'channel' => $searchResult['snippet']['channelTitle'], 208 | 'title' => $searchResult['snippet']['title'], 209 | 'full_link' => 'https://youtube.com/watch?v='.$searchResult['id']['videoId'] 210 | ); 211 | } 212 | 213 | return response()->json(['error' => false, 'message' => '', 'results' => $results]); 214 | 215 | } catch (Exception $ex) { 216 | $errorObj = json_decode($ex->getMessage()); 217 | return response()->json(['error' => true, 'message' => $errorObj->error->message], Response::HTTP_BAD_REQUEST); 218 | } 219 | } 220 | 221 | 222 | } 223 | -------------------------------------------------------------------------------- /src/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | has('q')) 22 | $response = Http::withToken($request->token)->get(route('youtube-api.search', $request->all())); 23 | else 24 | $response = Http::withToken($request->token)->post(route('youtube-api.convert'), $request->all()); 25 | 26 | if($response->status() == Response::HTTP_UNAUTHORIZED || $response->status() == Response::HTTP_TOO_MANY_REQUESTS) 27 | return back()->with('error', $response->object()->message); 28 | else if($response->status() == Response::HTTP_BAD_REQUEST) { 29 | $errObj = $response->object(); 30 | 31 | if(property_exists($errObj, 'error_messages')) 32 | return redirect()->back()->withErrors($errObj->error_messages); 33 | 34 | return back()->with('error', $errObj->message); 35 | } 36 | 37 | return redirect()->back()->with($request->has('q') ? 'searched' : 'converted', $response->object()); 38 | } 39 | 40 | public function logs(Request $request) 41 | { 42 | abort_if(config('youtube-api.enable_logging', false) === false, Response::HTTP_NOT_FOUND); 43 | 44 | $selectedDate = $request->get('date'); 45 | 46 | /** @var Collection $dates */ 47 | $dates = Log::select(DB::raw('DATE(created_at) date')) 48 | ->groupBy(DB::raw('DATE(created_at)')) 49 | ->orderBy(DB::raw('DATE(created_at)'), 'DESC') 50 | ->get(); 51 | 52 | $dates = $dates->map(fn($date) => $date->date); 53 | 54 | $logs = Log::where(DB::raw('DATE(created_at)'), $request->get('date', $dates->first())) 55 | ->orderBy('created_at', 'DESC') 56 | ->simplePaginate(25) 57 | ->withQueryString(); 58 | 59 | return view('youtube-api-views::logs.index', [ 60 | 'dates' => $dates, 61 | 'logs' => $logs, 62 | 'selectedDate' => $selectedDate, 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Drivers/Cobalt.php: -------------------------------------------------------------------------------- 1 | url = $url; 18 | $this->format = $format; 19 | } 20 | 21 | public function convert(): Video 22 | { 23 | if (config('youtube-api.cobalt.url') === null) 24 | throw new Exception('Cobalt API url is not configured'); 25 | 26 | $request = Http::accept('application/json'); 27 | 28 | if (config('youtube-api.cobalt.auth') !== null) 29 | $request->withHeaders(['Authorization' => config('youtube-api.cobalt.auth')]); 30 | 31 | $response = $request->post(config('youtube-api.cobalt.url'), [ 32 | 'url' => $this->url, 33 | 'downloadMode' => $this->format == 'mp3' ? 'audio' : 'auto', 34 | 'youtubeHLS' => config('youtube-api.cobalt.hls', false) 35 | ]); 36 | 37 | if ($response->status() != 200) 38 | throw new Exception($response->body()); 39 | 40 | $cobalt = $response->object(); 41 | 42 | if ($cobalt->status != 'tunnel') 43 | throw new Exception('This status is not supported yet'); 44 | 45 | $id = Video::getVideoId($this->url); 46 | 47 | if (env('GOOGLE_API_KEY') === null) 48 | { 49 | $file = explode('_', $cobalt->filename); 50 | $cobalt->filename = $file[1] . '.' . $this->format; 51 | 52 | if (!Storage::disk('public')->exists($cobalt->filename)) 53 | { 54 | $success = Storage::disk('public') 55 | ->put($cobalt->filename, file_get_contents($cobalt->url)); 56 | 57 | if (!$success) 58 | throw new Exception('Failed to save file'); 59 | } 60 | 61 | $url = Video::getDownloadUrl($cobalt->filename); 62 | } 63 | else 64 | { 65 | $url = $cobalt->url; 66 | } 67 | 68 | [$title, $duration, $uploadedAt] = $this->getMetaData(); 69 | 70 | $video = new Video( 71 | $id, 72 | $title, 73 | $url 74 | ); 75 | 76 | $video->setUploadedAt($uploadedAt); 77 | $video->setDuration($duration); 78 | 79 | return $video; 80 | } 81 | 82 | /** 83 | * @todo use some kind of cache, to prevent multiple requests or analyzes. The chances of the video changing title or (for sure) duration are ultra low 84 | */ 85 | private function getMetaData(): array 86 | { 87 | $id = Video::getVideoId($this->url); 88 | 89 | if (env('GOOGLE_API_KEY') === null) 90 | { 91 | $getID3 = new \getID3(); 92 | $fileInfo = $getID3->analyze(Video::getDownloadPath($id . '.' . $this->format)); 93 | 94 | return [ 95 | $fileInfo['tags'][$this->format == 'mp3' ? 'id3v2' : 'quicktime']['title'][0], 96 | $fileInfo['playtime_seconds'], 97 | null 98 | ]; 99 | } 100 | else 101 | { 102 | //todo same as /info endpoint 103 | $gClient = new \Google_Client(); 104 | $gClient->setDeveloperKey(env('GOOGLE_API_KEY')); 105 | 106 | $youtube = new \Google_Service_YouTube($gClient); 107 | $response = $youtube->videos->listVideos('snippet,contentDetails', ['id' => $id]); 108 | $ytVideo = $response->getItems()[0] ?? null; 109 | 110 | if ($ytVideo == null) 111 | throw new Exception('Video not found'); 112 | 113 | $duration = $ytVideo->getContentDetails()->getDuration(); 114 | $interval = new \DateInterval($duration); 115 | $duration = ($interval->h * 60 * 60) + ($interval->i * 60) + $interval->s; 116 | 117 | return [ 118 | $ytVideo->getSnippet()->getTitle(), 119 | $duration, 120 | new \DateTime($ytVideo->getSnippet()->getPublishedAt()) 121 | ]; 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/Drivers/IDriver.php: -------------------------------------------------------------------------------- 1 | youtubeDl = new YoutubeDl(); 20 | $this->format = $format; 21 | 22 | $this->options = Options::create() 23 | ->noPlaylist() 24 | ->downloadPath(Video::getDownloadPath()) 25 | ->proxy(config('youtube-api.proxy')) 26 | ->url($url); 27 | } 28 | 29 | public function convert(): Video 30 | { 31 | $options = $this->options->output('%(id)s.%(ext)s'); 32 | 33 | if($this->format == 'mp3') 34 | { 35 | $options = $options->extractAudio(true) 36 | ->audioFormat('mp3') 37 | ->audioQuality('0'); 38 | 39 | if(config('youtube-api.ffmpeg_path') !== null) { 40 | $options = $options->ffmpegLocation(config('youtube-api.ffmpeg_path')); 41 | } 42 | } 43 | else 44 | $options = $options->format('bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best'); 45 | 46 | $id = Video::getVideoId($this->options->getUrl()[0]); 47 | 48 | if (Storage::disk('public')->exists($id . '.' . $this->format)) 49 | $ytdlVideo = $this->getVideoWithoutDownload(); 50 | else 51 | $ytdlVideo = $this->youtubeDl->download($options)->getVideos()[0]; 52 | 53 | if ($ytdlVideo->getError() !== null) 54 | throw new Exception($ytdlVideo->getError()); 55 | 56 | $video = new Video( 57 | $ytdlVideo->getId(), 58 | $ytdlVideo->getTitle(), 59 | Video::getDownloadUrl($ytdlVideo->getId() . '.' . $this->format) 60 | ); 61 | 62 | $video->setUploadedAt($ytdlVideo->getUploadDate()); 63 | $video->setDuration($ytdlVideo->getDuration()); 64 | 65 | return $video; 66 | } 67 | 68 | public function getVideoWithoutDownload(): \YoutubeDl\Entity\Video 69 | { 70 | //todo kinda same as /info endpoint 71 | $ytdlVideo = $this->youtubeDl->download( 72 | $this->options->skipDownload(true) 73 | )->getVideos()[0]; 74 | 75 | return $ytdlVideo; 76 | } 77 | } -------------------------------------------------------------------------------- /src/Models/Log.php: -------------------------------------------------------------------------------- 1 | belongsTo(config('auth.providers.users.model')); 20 | else 21 | throw new Exception('No user provider model defined.'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Models/Video.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->title = $title; 25 | $this->file = $file; 26 | } 27 | 28 | public function getId(): string 29 | { 30 | return $this->id; 31 | } 32 | 33 | public function getTitle(): string 34 | { 35 | return $this->title; 36 | } 37 | 38 | public function getFile(): string 39 | { 40 | return $this->file; 41 | } 42 | 43 | public function getUploadedAt(): ?\DateTimeInterface 44 | { 45 | return $this->uploadedAt; 46 | } 47 | 48 | public function getDuration(): float 49 | { 50 | return $this->duration; 51 | } 52 | 53 | public function setUploadedAt(?\DateTimeInterface $uploadedAt): self 54 | { 55 | $this->uploadedAt = $uploadedAt; 56 | 57 | return $this; 58 | } 59 | 60 | public function setDuration(float $duration): self 61 | { 62 | $this->duration = $duration; 63 | 64 | return $this; 65 | } 66 | 67 | public static function getDownloadPath(string $file = ''): string 68 | { 69 | return Storage::disk('public')->path($file); 70 | } 71 | 72 | public static function getDownloadUrl(string $file = ''): string 73 | { 74 | return Storage::disk('public')->url($file); 75 | } 76 | 77 | public static function getVideoId(string $url): string 78 | { 79 | preg_match(static::URL_REGEX, $url, $matches); 80 | return $matches[0]; 81 | } 82 | 83 | public function toArray(): array 84 | { 85 | return [ 86 | 'youtube_id' => $this->getId(), 87 | 'title' => $this->getTitle(), 88 | 'file' => $this->getFile(), 89 | 'uploaded_at' => $this->getUploadedAt(), 90 | 'duration' => $this->getDuration() 91 | ]; 92 | } 93 | } -------------------------------------------------------------------------------- /src/YoutubeAPIServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 18 | $this->publishes([ 19 | __DIR__ . '/../config/youtube-api.php' => config_path('youtube-api.php') 20 | ], 'youtube-api-config'); 21 | } 22 | 23 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'youtube-api-views'); 24 | $this->loadRoutes(); 25 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 26 | } 27 | 28 | /** 29 | * Register the application services. 30 | */ 31 | public function register() 32 | { 33 | $this->mergeConfigFrom(__DIR__.'/../config/youtube-api.php', 'laravel-youtube-api'); 34 | } 35 | 36 | private function loadRoutes() { 37 | $apiMiddleware = ['api']; 38 | 39 | if(config('youtube-api.auth') !== null) { 40 | $apiMiddleware[] = 'auth:' . config('youtube-api.auth'); 41 | } 42 | 43 | if(config('youtube-api.ratelimiter') !== null) { 44 | RateLimiter::for('yt-api', config('youtube-api.ratelimiter')); 45 | 46 | $apiMiddleware[] = 'throttle:yt-api'; 47 | } 48 | 49 | Route::prefix('api/' . config('youtube-api.route_prefix', 'ytconverter')) 50 | ->middleware($apiMiddleware) 51 | ->namespace( 'MichaelBelgium\YoutubeAPI\Controllers') 52 | ->group(__DIR__.'/../routes/api.php'); 53 | 54 | Route::prefix(config('youtube-api.route_prefix', 'ytconverter')) 55 | ->middleware('web') 56 | ->namespace('MichaelBelgium\YoutubeAPI\Controllers') 57 | ->group(__DIR__.'/../routes/web.php'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/ApiTest.php: -------------------------------------------------------------------------------- 1 | post('/ytconverter/convert'); 17 | $response->assertSuccessful(); 18 | } 19 | } 20 | --------------------------------------------------------------------------------