From: Vincent Vanwaelscappel Date: Fri, 6 Jan 2023 09:44:57 +0000 (+0100) Subject: . X-Git-Url: http://git.cubedesigners.com/?a=commitdiff_plain;h=e44ab70695dc1b28097b47a0c959bb12bf81bb35;p=songbook.git . --- diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7fa6570..2bf8bf2 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -5,8 +5,7 @@ namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; -class Kernel extends ConsoleKernel -{ +class Kernel extends ConsoleKernel { /** * The Artisan commands provided by your application. * @@ -19,12 +18,12 @@ class Kernel extends ConsoleKernel /** * Define the application's command schedule. * - * @param \Illuminate\Console\Scheduling\Schedule $schedule + * @param \Illuminate\Console\Scheduling\Schedule $schedule * @return void */ - protected function schedule(Schedule $schedule) - { + protected function schedule(Schedule $schedule) { $schedule->command('job:dispatchNow PitchShiftAudio')->hourly(); + $schedule->command('job:dispatchNow SyncNotion')->everyTenMinutes(); } /** @@ -32,9 +31,8 @@ class Kernel extends ConsoleKernel * * @return void */ - protected function commands() - { - $this->load(__DIR__.'/Commands'); + protected function commands() { + $this->load(__DIR__ . '/Commands'); require base_path('routes/console.php'); } diff --git a/app/Http/Controllers/FrontController.php b/app/Http/Controllers/FrontController.php index 39a7413..5df42fd 100644 --- a/app/Http/Controllers/FrontController.php +++ b/app/Http/Controllers/FrontController.php @@ -8,17 +8,20 @@ use App\Models\Song; use Cubist\Backpack\Http\Controllers\CubistPWAController; use Cubist\Util\Files\Files; use Illuminate\Support\Facades\Session; +use Notion\Blocks\BlockInterface; +use Notion\Blocks\Paragraph; +use Notion\Common\RichText; +use Notion\Notion; +use Notion\Pages\Properties\RichTextProperty; +use Notion\Pages\Properties\Title; -class FrontController extends Controller -{ - public static function defaultCollection() - { +class FrontController extends Controller { + public static function defaultCollection() { return redirect('/' . Collection::withoutGlobalScope('ownerclause')->find(1)->slug); } - public function collection($name) - { + public function collection($name) { $collection = Collection::withoutGlobalScope('ownerclause')->where('slug', $name)->first(); if (null === $collection) { return self::defaultCollection(); @@ -26,14 +29,27 @@ class FrontController extends Controller if ($p = $this->checkPassword($collection)) { return $p; } + + if ($collection->notion_key && $collection->notion_database) { + return $this->notionCollection($collection); + } + $lists = CollectionList::withoutGlobalScope('ownerclause')->where('collection', $collection->id)->get(); $songs = $this->_getSongsOfCollection($collection->id, $lists); return view('collection', ['menu' => true, 'songs' => $songs, 'collection' => $collection, 'collection_songs' => $songs, 'collection_lists' => $lists]); } - public function checkPassword(Collection $collection) - { + /** + * @throws \Exception + */ + public function notionCollection($collection) { + $songs = self::getSongsOfNotionCollection($collection); + return view('notion_collection', ['menu' => true, 'notion' => true, 'songs' => $songs, 'collection' => $collection, 'collection_songs' => $songs]); + } + + + public function checkPassword(Collection $collection) { if (!$collection->password) { return false; } @@ -51,13 +67,15 @@ class FrontController extends Controller return view('login', ['menu' => false, 'collection' => $collection, 'error' => $error]); } - public function song($collection, $song) - { + public function song($collection, $song) { /** @var Collection $collection */ $collection = Collection::withoutGlobalScope('ownerclause')->where('slug', $collection)->first(); if (null === $collection) { abort(404); } + if ($collection->notion_key && $collection->notion_database) { + return $this->notionSong($collection, $song); + } /** @var Song $song */ $song = Song::withoutGlobalScope('ownerclause')->where('slug', $song)->first(); if (null === $song) { @@ -66,6 +84,8 @@ class FrontController extends Controller if ($p = $this->checkPassword($collection)) { return $p; } + + $lists = CollectionList::withoutGlobalScope('ownerclause')->where('collection', $collection->id)->get(); $partition = false; $lyrics_html = ''; @@ -102,8 +122,21 @@ class FrontController extends Controller return view('song', ['menu' => true, 'lyrics_html' => $lyrics_html, 'song' => $song, 'collection' => $collection, 'partition' => $partition, 'collection_songs' => $this->_getSongsOfCollection($collection->id, $lists), 'collection_lists' => $lists]); } - protected function _getSongsOfCollection($id, $lists) - { + protected function notionSong($collection, $song) { + $partition = false; + + $u = $song . '.html'; + $songs = self::getSongsOfNotionCollection($collection); + foreach ($songs as $song) { + if ($song->getUrl() === $u) { + break; + } + } + $lyrics_html = $song->getLyrics(); + return view('notion_song', ['menu' => true, 'notion' => true, 'lyrics_html' => $lyrics_html, 'song' => $song, 'collection' => $collection, 'collection_songs' => $songs]); + } + + protected function _getSongsOfCollection($id, $lists) { /** @var Song $q */ $q = Song::withoutGlobalScope('ownerclause')->whereRaw('json_contains(collections, \'["' . $id . '"]\')'); foreach ($lists as $list) { @@ -113,8 +146,145 @@ class FrontController extends Controller } - public function manifest($collection) - { + /** + * @param $collection Collection + * @return \App\Notion\Song[] + * @throws \Exception + */ + public static function getSongsOfNotionCollection($collection, $force = false) { + $cacheKey = 'songs_notion_' . $collection->notion_database . '_' . $collection->id; + $ttl = 900; + if ($force) { + $v = self::_getSongsOfNotionCollection($collection); + cache()->set($cacheKey, $v, $ttl); + } else { + return cache()->remember($cacheKey, $ttl, function () use ($collection) { + return self::_getSongsOfNotionCollection($collection); + }); + } + } + + + protected static function _getSongsOfNotionCollection($collection) { + + $audios = ['mp3', 'm4a', 'wav']; + $notion = Notion::create($collection->notion_key); + $database = $notion->databases()->find($collection->notion_database); + $songs = $notion->databases()->queryAllPages($database); + + $res = []; + foreach ($songs as $song) { + $pageContents = ''; + try { + $contents = $notion->blocks()->findChildren($song->id); + foreach ($contents as $content) { + $pageContents .= self::_blockToHtml($content); + } + } catch (\Exception $e) { + + } + + $s = []; + + /** @var Title $titleProperty */ + $titleProperty = $song->getProprety('Titre'); + /** @var Number $orderProperty */ + $orderProperty = $song->getProprety('Ordre'); + /** @var RichTextProperty $lyricsProperty */ + $lyricsProperty = $song->getProprety('Paroles'); + /** @var \Notion\Pages\Properties\Files $filesProperty */ + $filesProperty = $song->getProprety('Files & media'); + + $s['id'] = $song->id; + $s['order'] = (int)$orderProperty->number; + $s['title'] = $titleProperty->toString(); + $s['lyrics'] = self::_blockToHtml($lyricsProperty); + $s['notes'] = $pageContents; + $s['notion_url'] = $song->url; + + + $s['files'] = []; + $s['audios'] = []; + foreach ($filesProperty->files as $file) { + $ext = self::parseExtensionFromAmazonURL($file->url); + if (in_array($ext, $audios)) { + $s['audios'][] = ['name' => self::parseFileNameFromAmazonURL($file->url), 'url' => $file->url, 'ext' => $ext]; + } else { + $s['files'][] = ['name' => self::parseFileNameFromAmazonURL($file->url), 'url' => $file->url, 'ext' => $ext]; + } + } + + $res[$s['order']] = new \App\Notion\Song($s, $collection->notion_database); + } + ksort($res); + return $res; + + } + + /** + * @param $block RichTextProperty|BlockInterface + * @return string + */ + protected static function _blockToHtml($block) { + $start = ''; + $end = ''; + if ($block instanceof Paragraph) { + $start = '

'; + $end = '

'; + } + $res = ''; + foreach ($block->text as $rt) { + $res .= self::_richTextToHtml($rt); + } + $res = $start . $res . $end; + return str_replace('
', '', $res); + } + + /** + * @param $rt RichText + * @return string + */ + protected static function _richTextToHtml($rt) { + $start = ''; + $end = ''; + if ($rt->annotations->isBold) { + $start = '' . $start; + $end = $end . ''; + } + if ($rt->annotations->isItalic) { + $start = '' . $start; + $end = $end . ''; + } + if ($rt->annotations->isUnderline) { + $start = '

' . $start; + $end = $end . '

'; + } + if ($rt->annotations->isCode) { + $start = '
' . $start; + $end = $end . '
'; + } + return $start . nl2br($rt->plainText) . $end; + } + + public static function parseFileNameFromAmazonURL($url) { + $u = parse_url($url); + $p = explode('/', $u['path']); + $f = array_pop($p); + $e = explode('.', $f); + array_pop($e); + $f = implode('.', $e); + return str_replace('_', ' ', $f); + } + + public static function parseExtensionFromAmazonURL($url) { + $u = parse_url($url); + $p = explode('/', $u['path']); + $f = array_pop($p); + $e = explode('.', $f); + return array_pop($e); + } + + public function manifest($collection) { $collection = Collection::withoutGlobalScope('ownerclause')->where('slug', $collection)->first(); if (null === $collection) { abort(404); @@ -133,21 +303,20 @@ class FrontController extends Controller $icons[count($icons) - 1]['purpose'] = 'maskable'; $res = ['name' => $collection->name, - 'short_name' => $collection->shortname ?: $collection->name, - 'description' => '', - 'display' => 'standalone', - 'orientation' => 'any', - 'background_color' => $collection->splashscreen_color, - 'theme_color' => $collection->theme_color, - 'start_url' => '/' . $collection->slug, - 'scope' => '/' . $collection->slug, - 'icons' => $icons]; + 'short_name' => $collection->shortname ?: $collection->name, + 'description' => '', + 'display' => 'standalone', + 'orientation' => 'any', + 'background_color' => $collection->splashscreen_color, + 'theme_color' => $collection->theme_color, + 'start_url' => '/' . $collection->slug, + 'scope' => '/' . $collection->slug, + 'icons' => $icons]; return response(json_encode($res))->header('Content-Type', 'application/manifest+json'); } - public function downloadAssets($songId) - { + public function downloadAssets($songId) { /** @var Song $song */ $song = Song::withoutGlobalScope('ownerclause')->find($songId); $fields = ['partition', 'lyrics_doc']; diff --git a/app/Jobs/DownloadAudioTracks.php b/app/Jobs/DownloadAudioTracks.php index 6fcf745..ad4fc64 100644 --- a/app/Jobs/DownloadAudioTracks.php +++ b/app/Jobs/DownloadAudioTracks.php @@ -3,19 +3,13 @@ namespace App\Jobs; use App\Models\Song; +use Cubist\Backpack\Jobs\Base; use Cubist\Util\Files\Files; use Cubist\Util\PHP; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; use YoutubeDl\Options; use YoutubeDl\YoutubeDl; -class DownloadAudioTracks implements ShouldQueue -{ - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; +class DownloadAudioTracks extends Base{ public function handle() { diff --git a/app/Jobs/OptimizeMP3.php b/app/Jobs/OptimizeMP3.php index ee56e9e..26a905e 100644 --- a/app/Jobs/OptimizeMP3.php +++ b/app/Jobs/OptimizeMP3.php @@ -5,10 +5,7 @@ namespace App\Jobs; use App\Models\Song; use Cubist\Backpack\Jobs\Base; use Cubist\Util\CommandLine; -use Cubist\Util\Files\Files; use Cubist\Util\PHP; -use YoutubeDl\Options; -use YoutubeDl\YoutubeDl; class OptimizeMP3 extends Base { diff --git a/app/Jobs/PitchShiftAudio.php b/app/Jobs/PitchShiftAudio.php index 7488186..6fee92a 100644 --- a/app/Jobs/PitchShiftAudio.php +++ b/app/Jobs/PitchShiftAudio.php @@ -3,6 +3,7 @@ namespace App\Jobs; use App\Models\Song; +use Cubist\Backpack\Jobs\Base; use Cubist\Util\CommandLine; use Cubist\Util\PHP; use Illuminate\Bus\Queueable; @@ -11,14 +12,11 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class PitchShiftAudio implements ShouldQueue -{ - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; +class PitchShiftAudio extends Base { public static $pitches = [1 => '+1', 2 => '+2', 3 => '+3', 4 => '+4', 5 => '+5', 6 => '+6', 7 => '-5', 8 => '-4', 9 => '-3', 10 => '-2', 11 => '-1']; - public function handle() - { + public function handle() { PHP::neverStop(); DownloadAudioTracks::dispatchSync(); diff --git a/app/Jobs/SyncNotion.php b/app/Jobs/SyncNotion.php new file mode 100644 index 0000000..147c2ce --- /dev/null +++ b/app/Jobs/SyncNotion.php @@ -0,0 +1,30 @@ +get() as $collection) { + if ($collection->notion_key && $collection->notion_database) { + FrontController::getSongsOfNotionCollection($collection, true); + } + } + } +} diff --git a/app/Models/Collection.php b/app/Models/Collection.php index 2815a60..6270f42 100644 --- a/app/Models/Collection.php +++ b/app/Models/Collection.php @@ -11,6 +11,7 @@ use Cubist\Backpack\Magic\Fields\SelectFromModel; use Cubist\Backpack\Magic\Fields\Slug; use Cubist\Backpack\Magic\Fields\Table; use Cubist\Backpack\Magic\Fields\Text; +use Cubist\Backpack\Magic\Fields\URL; use Cubist\Backpack\Magic\Models\CubistMagicAbstractModel; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; @@ -47,6 +48,9 @@ class Collection extends CubistMagicAbstractModel { $this->addField('agenda', FilesOrURL::class, 'Planning / Agenda', ['default' => false, 'database_default' => false]); $this->addField('organisation_name', Text::class, 'Label du lien "Répartition / organisation"', ['default' => false, 'database_default' => false]); $this->addField('organisation', FilesOrURL::class, 'Répartition / organisation', ['default' => false, 'database_default' => false]); + $this->addField('notion_key',Text::class,'Clé Notion'); + $this->addField('notion_database',Text::class,'Base de données Notion'); + $this->addField('notion_home',URL::class,'Home Notion'); } protected function _getFreeFileBaseDirectory() { diff --git a/app/Notion/Song.php b/app/Notion/Song.php new file mode 100644 index 0000000..101fefc --- /dev/null +++ b/app/Notion/Song.php @@ -0,0 +1,55 @@ +getTitle()) . '.html'; + } + + public function getArtist() { + return ''; + } + + public function getTitle() { + return $this->get('title'); + } + + public function getLyrics() { + return $this->get('lyrics'); + } + + public function getId() { + return $this->get('id'); + } + + public function getOrder() { + return $this->get('order'); + } + + public function getNotionURL(){ + return $this->get('notion_url'); + } + + public function hasSimpleChords(){ + return false; + } + + public function getAudioTracks() { + $res = $this->get('audios'); + foreach ($res as $k=>$v) { + $res[$k]['i']=$k; + $res[$k]['opturl']=$v['url']; + } + return $res; + } + + public function getFiles(){ + return $this->get('files'); + } + +} diff --git a/composer.json b/composer.json index 3ed4667..5713631 100644 --- a/composer.json +++ b/composer.json @@ -14,21 +14,26 @@ ], "license": "MIT", "require": { - "php": ">=7.4", + "php": ">=8.1", + "ext-dom": "*", "ext-json": "*", + "ext-libxml": "*", + "ext-redis": "*", "ext-simplexml": "*", "ext-tidy": "*", "ext-zip": "*", - "ext-libxml": "*", - "ext-dom": "*", - "ext-redis": "*", "cubist/cms-back": "dev-master", "fruitcake/laravel-cors": "^2.2", + "guzzlehttp/psr7": "^2.0", "league/csv": "^9.8", + "mariosimao/notion-sdk-php": "^1.0", "mxl/laravel-job": "^1.3", "norkunas/youtube-dl-php": "dev-master", "php-ffmpeg/php-ffmpeg": "^0.18.0", - "phpoffice/phpspreadsheet": "^1.23" + "phpoffice/phpspreadsheet": "^1.25" + }, + "replace": { + "cviebrock/laravel-elasticsearch": "*" }, "require-dev": { "barryvdh/laravel-ide-helper": "^2.10", diff --git a/resources/css/app.sass b/resources/css/app.sass index 54a9a9d..c1e914d 100644 --- a/resources/css/app.sass +++ b/resources/css/app.sass @@ -42,6 +42,12 @@ font-weight: 700 src: url('fonts/roboto-condensed-700.woff2') format('woff2') +@font-face + font-family: 'Dancing Script' + font-style: normal + font-weight: 400 + src: url('fonts/dancing-script-v24-latin-regular.woff2') format('woff2') + $slab: 'Roboto Slab', sans-serif .phpdebugbar @@ -106,11 +112,15 @@ main overflow: hidden text-overflow: ellipsis + span color: #000 @media (prefers-color-scheme: dark) color: #fff font-size: 0.65em + &.order + font-size: 1em + margin-right: 10px li list-style: none @@ -136,6 +146,17 @@ article.song @media (prefers-color-scheme: dark) color: #fff !important + &.fromnotion + margin: 0 !important + font-family: $slab !important + border-bottom: 1px solid currentColor + padding-bottom: 20px + + &.notes + font-family: "Dancing Script", cursive !important + padding-top: 20px + font-size: 1.25em + &.lyrics .lyrics @@ -206,10 +227,27 @@ article.song top: -0.25em opacity: 0.5 + h2 + font-size: 20px + margin: 20px 0 15px + + ul + li + list-style: none + h3 color: var(--theme-color) margin: 5px 0 + a.select_audio + cursor: pointer + + blockquote + border-left: 5px solid var(--theme-color) + padding-left: 20px + opacity: 0.6 + + .lyrics font-family: "Roboto Slab", serif font-weight: 400 diff --git a/resources/css/fonts/dancing-script-v24-latin-regular.woff2 b/resources/css/fonts/dancing-script-v24-latin-regular.woff2 new file mode 100644 index 0000000..a78f79d Binary files /dev/null and b/resources/css/fonts/dancing-script-v24-latin-regular.woff2 differ diff --git a/resources/js/mmenu.js b/resources/js/mmenu.js index 2b34144..f483c67 100644 --- a/resources/js/mmenu.js +++ b/resources/js/mmenu.js @@ -34,6 +34,13 @@ document.addEventListener("DOMContentLoaded", () => { $('body').addClass('init'); }); +setTimeout(function () { + $('body').addClass('init'); +}, 2000); +$(function () { + $('body').addClass('init'); +}); + $(window).on('beforeunload', function () { $('body').removeClass('init'); }); @@ -103,12 +110,11 @@ window.setOption = function (name, value) { $('input[name="' + n + '"]').prop('checked', value == '1'); } -function updateSelect(select) { +window.updateSelect = function (select) { var sv = $(select).val(); if (sv === null) { return; } - console.log('Set option from select', $(select).data('name'), sv); $(select).closest('.clickselect').find('span').text($(select).find('option[value="' + $(select).val() + '"]').text()); setOption($(select).data('name'), $(select).val()); updateSongView(); @@ -124,6 +130,7 @@ $(function () { return true; }); + $(document).on('change', '.checkbox-switch', function () { var name = $(this).attr('name'); var checked = $(this).is(':checked') ? '1' : '0'; diff --git a/resources/js/player.js b/resources/js/player.js index 37a9560..0046519 100644 --- a/resources/js/player.js +++ b/resources/js/player.js @@ -9,6 +9,16 @@ resetPlayers(); }); + $(document).on('click', '.select_audio', function () { + var e = $(this).attr('data-id').split('_'); + $('select[data-name="audio"]').val(e[1]); + updateSelect($('select[data-name="audio"]')); + resetPlayers(); + playAudio(); + return false; + }); + + function resetPlayers() { $.each(window.players, function (k, player) { player.stop(); @@ -30,7 +40,12 @@ }); }); $(document).on('click', '[data-action="play"]', function () { - $(this).hide(); + playAudio(); + return false; + }); + + function playAudio() { + $('[data-action="play"]').hide(); var audio = window.getOption('audio').toString(); var t = window.getOption('audio_' + audio + '_tone'); @@ -46,8 +61,7 @@ var p = window.players[pid]; $(p.elements.container).show(); p.play(); - return false; - }); + } function changeAudio() { $('li[data-audio]').hide(); diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php index 11bf20e..43068c7 100644 --- a/resources/views/layout.blade.php +++ b/resources/views/layout.blade.php @@ -1,15 +1,19 @@ +@if(isset($notion) && $notion) +@include('notion_menu') +@else @include('menu') - +@endif + @yield('title') @@ -32,7 +36,7 @@ diff --git a/resources/views/notion_collection.blade.php b/resources/views/notion_collection.blade.php new file mode 100644 index 0000000..ff85e94 --- /dev/null +++ b/resources/views/notion_collection.blade.php @@ -0,0 +1,18 @@ +@extends('layout') +@php + $preload=[]; +@endphp +@section('title', $collection->name) + +@section('content') + @include('header',['title'=>$collection->name,'subtitle'=>'']) +
+ +
+@endsection diff --git a/resources/views/notion_collection_song.blade.php b/resources/views/notion_collection_song.blade.php new file mode 100644 index 0000000..6034632 --- /dev/null +++ b/resources/views/notion_collection_song.blade.php @@ -0,0 +1,5 @@ +@php + $songurl='/'.$collection->slug.'/'.$csong->getURL(); +@endphp +
  • {{$csong->getOrder()}} {{$csong->getTitle()}}{{$csong->getArtist()}} +
  • diff --git a/resources/views/notion_menu.blade.php b/resources/views/notion_menu.blade.php new file mode 100644 index 0000000..44c0be5 --- /dev/null +++ b/resources/views/notion_menu.blade.php @@ -0,0 +1,144 @@ +@php + $tones=\App\Field\Tone::getTones(); + $fontSizes=[0.5,0.6,0.7,0.8,0.9,1,1.1,1.25,1.5,1.75,2]; + $prefetch=[]; +@endphp + +@section('menu') + +@endsection + +@section('prefetch') + +@endsection diff --git a/resources/views/notion_menu_song.blade.php b/resources/views/notion_menu_song.blade.php new file mode 100644 index 0000000..334b59a --- /dev/null +++ b/resources/views/notion_menu_song.blade.php @@ -0,0 +1,6 @@ +@php + $songurl="/$collection->slug/".$csong->getUrl(); + $prefetch[]=$songurl; +@endphp +
  • {{trim($csong->getOrder().' - '.$csong->getTitle().' - '.$csong->getArtist(),' -')}} +
  • diff --git a/resources/views/notion_song.blade.php b/resources/views/notion_song.blade.php new file mode 100644 index 0000000..c3c0117 --- /dev/null +++ b/resources/views/notion_song.blade.php @@ -0,0 +1,51 @@ +@php + $audioTracks=$song->getAudioTracks(); + $simple=$song->hasSimpleChords(); +@endphp + +@extends('layout') +@section('title', $song->title.' - '.$song->artist.' - '. $collection->name) +@section('content') + @include('header',['title'=>$song->title,'subtitle'=>$song->artist]) +
    +
    {!! $lyrics_html !!}
    + @if($song->notes) +
    {!! $song->notes !!}
    + @endif +
    {{$song->credits}}
    + @if(count($audioTracks)) +

    💿 {{__('Audios')}}

    + + @endif + @if(count($song->getFiles())) +

    📄 {{__('Fichiers')}}

    + + @endif +
    + @if(count($audioTracks)) +
    + @foreach($audioTracks as $audio) + + @endforeach +
    + @endif +@endsection +@section('floating') + @if(count($audioTracks)) + + + + @endif +@endsection