Phalcon Framework 4.1.2

Error: Call to a member function setShared() on null

/var/www/html/app/config/services.php (186)
#0Closure->{closure}
#1Phalcon\Di\Service->resolve
#2Phalcon\Di->get
#3Phalcon\Di->getShared
#4Phalcon\Mvc\Model\Manager->getConnection
#5Phalcon\Mvc\Model\Manager->getReadConnection
#6Phalcon\Mvc\Model->getReadConnection
#7Phalcon\Mvc\Model\Query->getReadConnection
#8Phalcon\Mvc\Model\Query->_executeSelect
#9Phalcon\Mvc\Model\Query->execute
#10Phalcon\Mvc\Model::findFirst
/var/www/html/app/controllers/IndexController.php (749)
<?php
declare(strict_types=1);
 
use Phalcon\Filter;
use Phalcon\Image\Adapter\Gd as Image;
use Phalcon\Paginator\Adapter\QueryBuilder as QueryPaginator;
use Phalcon\Paginator\Adapter\NativeArray as ArrayPaginator;
use Musikord\Paginator\Adapter\Model as ModelPaginator;
use Musikord\Performance\Caching;
use Musikord\StaticList\VideoCategory;
use Musikord\StaticList\NewsCategory;
use Musikord\StaticList\Genre as GenreList;
 
use Phalcon\Mvc\View;
use Carbon\Carbon;
 
class IndexController extends ControllerBase
{
    private $meta;
    private $meta_properties;
 
    public $headerAssets;
    public $footerAssets;
    
    public function initialize()
    {
        parent::initialize();
    
        $this->headerAssets = $this->assets->collection('headerAssets');
    
        /* --- Styling moved to HTML, comment out this block ---
        echo '<link rel="preload" href="https://www.musikord.com/css/font-awesome/fonts/fontawesome-webfont.woff2?v=4.7.0" as="font" type="font/woff2" crossorigin>';
        echo '<link rel="preload" href="https://www.musikord.com/css/material-design-icons/MaterialIcons-Regular.woff2" as="font" type="font/woff2" crossorigin>';
        echo '<link rel="preload" href="https://www.musikord.com/css/bootstrap/dist/css/bootstrap.min.css" as="style">';
        echo '<link rel="preload" href="https://www.musikord.com/css/styles/app-min.css" as="style">';
        
        $this->headerAssets
            ->addCss('https://www.musikord.com/css/font-awesome/css/font-awesome.min.css', false)
            ->addCss('https://www.musikord.com/css/material-design-icons/material-design-icons.css', false)
            ->addCss('https://www.musikord.com/css/bootstrap/dist/css/bootstrap.min.css', false)
            ->addCss('https://www.musikord.com/css/styles/app-min.css', false)
            ->addCss('https://www.musikord.com/css/styles/style.css', false)
            ->addCss('https://www.musikord.com/css/styles/font.css', false)
            ->addCss('https://www.musikord.com/css/styles/custom.css', false)
            ->addCss('https://www.musikord.com/js/owl.carousel/dist/assets/owl.carousel.min.css', false)
            ->addCss('https://www.musikord.com/js/owl.carousel/dist/assets/owl.theme.css', false)
            ->addCss('https://www.musikord.com/css/theme/danger.css', false);
        --- End Styling Block --- */
        
        $this->footerAssets = $this->assets->collection('footerAssets');
        
        /*
        $this->footerAssets
            ->addJs('https://www.musikord.com/js/jquery/dist/jquery.min.js', true)
            ->addJs('https://www.musikord.com/js/bootstrap/dist/js/bootstrap.js', true)
            ->addJs('https://www.musikord.com/js/owl.carousel/dist/owl.carousel.min.js', true)
            ->addJs('https://www.musikord.com/js/jquery-pjax/jquery.pjax.js', true)
            ->addJs('https://www.musikord.com/js/sticky-kit/jquery.sticky-kit.min.js', true)
            ->addJs('https://www.musikord.com/js/config.lazyload.js', true)
            ->addJs('https://www.musikord.com/js/ui-load.js', true)
            ->addJs('https://www.musikord.com/js/ui-jp.js', true)
            ->addJs('https://www.musikord.com/js/ajax.js', true)
            ->addJs('https://www.musikord.com/js/jquery.transposer.js', true);
        */   
        
        // SEO Tags
        $this->tag->setTitle('Musikord.com');
        $this->tag->setTitleSeparator(' @ ');
        $this->view->search_query = '';
    
        $this->meta = [
            'description' => '#1 site of databases of Chords, Tabs, Lyrics and Videos that you can access for free, forever!'
        ];
    
        $this->meta_properties = [
            'og:url' => $this->url->get($this->request->get('_url')),
            'og:type' => 'music.song',
            'og:title' => $this->tag->getTitle(),
            'og:description' => 'CHORDS by {{current_artist.title}}'
        ];
    
        $this->view->meta = $this->meta;
        $this->view->meta_property = $this->meta_properties;
        $this->view->canonical_url = $this->request->get('_url');
    
        $point = 0;
        if ($this->session->has('user')) {
            $point = Point::sum([
                'column' => 'point',
                'user = ' . $this->session->get('user')->id
            ]) ?? 0;
        }
    
        $this->view->point = $point;
    }
 
    public function indexAction()
    {
       // $this->assets->addInlineCss(<<<CSSSCRIPT
       //     .owl-dots {
       //       position: absolute;
       //       float: center;
       //       right: 0;
       //       margin-top: -30px;
       //     }
       // CSSSCRIPT);
    
        $this->tag->appendTitle('#1 site of databases of Chords, Tabs, Lyrics and Videos that you can access for free, forever!');
    
        $cache = $this->cache;
    
        // Sponsor ad (optimized)
        $sponsor = $cache->get('sponsor');
        if ($sponsor === null) {
            $maxId = Ads::maximum(['column' => 'id']);
            $randomId = rand(1, (int) $maxId);
            $sponsor = Ads::findFirst([
                'conditions' => 'id >= :id:',
                'bind' => ['id' => $randomId],
                'order' => 'id ASC'
            ]);
            if (!$sponsor) {
                // fallback to first row
                $sponsor = Ads::findFirst();
            }
            $cache->set('sponsor', $sponsor, 3600);
        }
        $this->view->sponsor = $sponsor;
    
        // Playlist
        // $playlist = $cache->get('playlist');
        // if ($playlist === null) {
            $playlist = Playlist::find([
                'order' => 'id DESC',
                'limit' => 10
            ]);
        //     $cache->set('playlist', $playlist, 3600);
        // }
        $this->view->playlist = $playlist;
    
        // Top artists
        $topArtist = $cache->get('top_artist');
        if ($topArtist === null) {
            $topArtist = Artist::find([
                'order' => 'view DESC',
                'limit' => 12
            ]);
            $cache->set('top_artist', $topArtist, 3600);
        }
        $this->view->top_artist = $topArtist;
    
        // Popular chords
        $popularChord = $cache->get('popular_chord');
        if ($popularChord === null) {
            $popularChord = Chord::find([
                'order' => 'view DESC',
                'limit' => 12
            ]);
            $cache->set('popular_chord', $popularChord, 3600);
        }
        $this->view->popular_chord = $popularChord;
        
        // Popular chords HOMEPAGE based on hits (not create_time)
        $popularChordHome = $cache->get('popular_chord_home_hits');
        
        if ($popularChordHome === null) {
            // Fetch the top 12 chords for today based on hit count
            $today = date('Y-m-d');
            $popularChordHome = $this->modelsManager->createBuilder()
                ->columns('chord_id, COUNT(*) AS hit_count')
                ->from(ChordHits::class)
                ->where('DATE(hit_time) = :today:', ['today' => $today])
                ->groupBy('chord_id')
                ->orderBy('hit_count DESC')
                ->limit(12)
                ->getQuery()
                ->execute();
        
            // If no hits today, fallback to top 12 chords for this week
            if (count($popularChordHome) === 0) {
                $popularChordHome = $this->modelsManager->createBuilder()
                    ->columns('chord_id, COUNT(*) AS hit_count')
                    ->from(ChordHits::class)
                    ->where('hit_time >= :week:', ['week' => date('Y-m-d H:i:s', strtotime('-7 days'))])
                    ->groupBy('chord_id')
                    ->orderBy('hit_count DESC')
                    ->limit(12)
                    ->getQuery()
                    ->execute();
            }
        
            // Cache the result for 1 hour
            $cache->set('popular_chord_home_hits', $popularChordHome, 3600);
        }
        
        // Prepare the list of Chord objects for rendering
        $chords = [];
        foreach ($popularChordHome as $hit) {
            $chords[] = Chord::findFirst($hit->chord_id); // Retrieve the full chord object
        }
        
        $this->view->popular_chord_home = $chords;
 
        // Newest chords
        $newestChord = $cache->get('newest_chord');
        if ($newestChord === null) {
            $newestChord = Chord::find([
                'artist IS NOT NULL',
                'order' => 'id DESC',
                'limit' => 12
            ]);
            $cache->set('newest_chord', $newestChord, 300);
        }
        $this->view->newest_chord = $newestChord;
    
        // Random chord (optimized)
        $randomChord = $cache->get('random_chord');
        if ($randomChord === null) {
            $maxId = Chord::maximum(['column' => 'id']);
            $ids = range(1, (int) $maxId);
            shuffle($ids);
            $randomIds = array_slice($ids, 0, 12);
    
            $randomChord = Chord::find([
                'conditions' => 'id IN ({ids:array}) AND artist IS NOT NULL',
                'bind' => ['ids' => $randomIds]
            ]);
    
            $cache->set('random_chord', $randomChord, 300);
        }
        $this->view->random_chord = $randomChord;
    
        $this->view->video_slider = [];
        
        $this->view->homepage_video = Video::find([
            'order' => 'id DESC',
            'limit' => 6
        ]);
    }
 
    public function artistByInitialAction($initial = '')
    {
        $initial = $this->dispatcher->getParam('initial');
    
        $this->tag->prependTitle('Artist by Letter ' . strtoupper($initial));
        $this->view->initial = $initial;
 
        $query = [
            'title LIKE :find:',
            'bind' => [
                'find' => $initial . '%'
            ],
            'order' => 'title ASC'
        ];
 
        if ($initial == '0-9') {
            $query = [
                "REGEXP(title, '^[0-9]+(.*)$')",
                'order' => 'title ASC'
            ];
        }
 
        $artists = new ModelPaginator([
            'model' => Artist::class,
            'parameters' => $query,
            'page' => $this->request->get('page'),
            'limit' => 31
        ]);
 
        $this->view->artists = $artists->paginate();
    }
 
    public function chordsByArtistAction()
    {
        $this->tag->prependTitle('CHORDS AND LYRICS by');
    }
 
    public function showArtistAction()
    {
        $slug = $this->dispatcher->getParam('slug');
        $initial = $this->dispatcher->getParam('initial');
 
        if ($slug == null) {
            return $this->response->redirect('404');
        }
 
        if (strtolower($initial) != strtolower(substr($slug, 0, 1))) {
            return $this->response->redirect('artists/' . strtolower(substr($slug, 0, 1)) . '/' . strtolower($slug) . '.html');
        }
 
        $isInitialCapital = preg_match('/[A-Z]/', $initial);
        $findCapital = preg_match('/[A-Z]/', $slug);
 
        if ($findCapital == 1 || $isInitialCapital) {
            $this->tag->prependTitle('Page Not Found');
            $this->response->setStatusCode(404, 'Page not found!');
            return $this->view->render('index', 'notFound404');
 
            // return $this->response->redirect(strtolower($this->request->get('_url')));
        }
 
        $artist = Artist::findFirst([
            'slug LIKE :slug:',
            'bind' => [
                'slug' => $slug
            ]
        ]);
 
        if (!$artist) {
            return $this->response->redirect('/404');
        }
 
        $artist->view++;
 
        $this->view->chord_list = Chord::find([
            'artist = :artist: OR featuring LIKE :find_featuring:',
            'bind' => [
                'artist' => $artist->id,
                'find_featuring' => '%"' . $artist->id . '"%'
            ]
        ]);
        
        $this->view->artist = $artist;
        
        // Set <title>
        $this->tag->setTitle('CHORDS AND LYRICS by ' . $artist->title . ' @ Musikord');
        
        // Meta description (optional: based on artist genre/tags/description if available)
        $description = 'Explore chords and lyrics by ' . $artist->title . ' at Musikord. Discover songs, genres, and featured artists.';
        
        // Basic meta tags
        $this->meta['description'] = $description;
        $this->meta['keywords'] = $artist->title . ', chords, lyrics, musikord, guitar chord, ukulele, piano';
        
        // Open Graph tags
        $this->meta_properties['og:title'] = $this->tag->getTitle();
        $this->meta_properties['og:description'] = $description;
        $this->meta_properties['og:url'] = 'https://www.musikord.com/artists/' . strtolower($initial) . '/' . $slug . '.html';
        $this->meta_properties['og:type'] = 'profile';
        
        // If artist artwork exists, use it for image preview
        if (!empty($artist->artwork)) {
            $imageUrl = $this->url->get($artist->artwork);
            $this->meta_properties['og:image'] = $imageUrl;
            $this->meta_properties['twitter:image'] = $imageUrl;
        }
        
        // Twitter card tags
        $this->meta_properties['twitter:card'] = 'summary_large_image';
        $this->meta_properties['twitter:site'] = '@Musikord';
        $this->meta_properties['twitter:creator'] = '@Musikord';
        $this->meta_properties['twitter:description'] = $description;
        $this->meta_properties['twitter:title'] = $this->tag->getTitle();
        
        // Assign meta to view
        $this->view->meta = $this->meta;
        $this->view->meta_property = $this->meta_properties;
        
        // Canonical URL
        $this->view->canonical_url = 'https://www.musikord.com/artists/' . strtolower($initial) . '/' . strtolower($slug) . '.html';
 
        $this->view->related_artists = Artist::find([
            'limit' => 12,
            'order' => 'RAND()'
        ]);
        
        $artist->tags = json_encode($artist->tags);
        $artist->genre = implode('|', $artist->genre);
        $artist->save();
    }
 
    public function genreAction()
    {
        $genre = $this->dispatcher->getParam('genre');
        $this->tag->prependTitle('Genre ' . $genre);
 
        $hasCapital = preg_match('/[A-Z]/', $genre);
 
        if ($hasCapital) {
            return $this->response->redirect('artists/genre/' . strtolower($genre));
        }
 
        $paginator = new ModelPaginator([
            'model' => Artist::class,
            'parameters' => [
                'genre LIKE :genre:',
                'bind' => [
                    'genre' => '%' . urldecode($genre) . '%'
                ]
            ],
            'limit' => 27,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        // Need to be cached
        $paginated = $paginator->paginate();
 
        $this->view->genre = $genre;
        $this->view->artists = $paginated;
    }
    
    public function trendingChordsAction()
    {
        $this->view->disableLevel([
            View::LEVEL_LAYOUT      => true,
            View::LEVEL_MAIN_LAYOUT => true,
        ]);
    
        $filter = $this->request->getQuery('filter', 'string', 'all');
        $description = 'All Time';
        $chordIds = [];
        $orderField = '';
    
        if ($filter === 'all') {
            // All Time: get top chords ordered by total views stored in Chord.view
            $description = 'All Time';
            $topChords = Chord::find([
                'conditions' => 'chord IS NOT NULL AND chord != ""',
                'order' => 'view DESC',
                'limit' => 20
            ]);
    
            foreach ($topChords as $chord) {
                $chordIds[] = $chord->id;
            }
    
            if (!empty($chordIds)) {
                $orderField = 'FIELD(id, ' . implode(',', $chordIds) . ')';
            }
        } else {
            // Filters for today, week, month - use chord_hits table
    
            $descriptionMap = [
                'today' => 'Today',
                'week' => 'This week',
                'month' => 'This month'
            ];
            $description = $descriptionMap[$filter] ?? 'All Time';
    
            $dateCondition = '';
            $bind = [];
    
            switch ($filter) {
                case 'today':
                    $dateCondition = 'hit_time BETWEEN :start: AND :end:';
                    $bind['start'] = date('Y-m-d 00:00:00');
                    $bind['end'] = date('Y-m-d 23:59:59');
                    break;
                case 'week':
                    $dateCondition = 'hit_time >= :date:';
                    $bind['date'] = date('Y-m-d H:i:s', strtotime('-7 days'));
                    break;
                case 'month':
                    $dateCondition = 'hit_time >= :date:';
                    $bind['date'] = date('Y-m-d H:i:s', strtotime('-30 days'));
                    break;
            }
    
            $builder = $this->modelsManager->createBuilder()
                ->columns('chord_id, COUNT(*) as views')
                ->from(ChordHits::class);
    
            if ($dateCondition) {
                $builder->where($dateCondition, $bind);
            }
    
            $builder
                ->groupBy('chord_id')
                ->orderBy('views DESC');
    
            $results = $builder->getQuery()->execute();
    
            foreach ($results as $row) {
                $chordIds[] = (int) $row->chord_id;
            }
    
            if (empty($chordIds)) {
                $chordIds = [0];
            }
    
            $orderField = 'FIELD(id, ' . implode(',', $chordIds) . ')';
        }
    
        // Fallback if no chords found at all (should not happen)
        if (empty($chordIds)) {
            $chordIds = [0];
        }
    
        $paginator = new ModelPaginator([
            'model' => Chord::class,
            'parameters' => [
                'conditions' => 'id IN ({ids:array})',
                'bind' => ['ids' => $chordIds],
                'order' => $orderField
            ],
            'limit' => 10,
            'page' => $this->request->get('page', 'int', 1),
        ]);
    
        $this->view->setVars([
            'popular_chord' => $paginator->paginate(),
            'description'   => $description,
        ]);
    
        $this->view->pick('components/trending_slider');
    }
 
    /*
    public function topChordsAction()
    {
        $this->tag->prependTitle('Top Chords');
    
        switch ($this->request->get('filter')) {
            case 'month':
                $this->view->description = 'This month';
    
                $params = [
                    'DATE(create_time) BETWEEN DATE(:date:) AND DATE(NOW())',
                    'bind' => [
                        'date' => date('Y-m-d', time() - 2592000)
                    ],
                    'order' => 'view DESC'
                ];
            break;
            
            case 'week':
                $this->view->description = 'This week';
                
                $params = [
                    'DATE(create_time) BETWEEN DATE(:date:) AND DATE(NOW())',
                    'bind' => [
                        'date' => date('Y-m-d', time() - 604800)
                    ],
                    'order' => 'view DESC'
                ];
            break;
            
            case 'today':
                $this->view->description = 'Today';
                
                $params = [
                    'DATE(create_time) = DATE(NOW())',
                    'order' => 'view DESC'
                ];
            break;
            
            default:
                $params = [
                    'order' => 'view DESC'
                ];
            break;
        }
    
        $chord = new ModelPaginator([
            'model' => Chord::class,
            'parameters' => $params,
            'limit' => 20,
            'page' => $this->request->get('page') ?? 1
        ]);
    
        $this->view->chords = $chord->paginate();
    }
    */
    
    public function topChordsAction()
    {
        $this->tag->prependTitle('Trending');
    
        $filter = $this->request->get('filter', 'string', 'all');
        $this->view->description = 'All Time';
        $chordIds = [];
        $orderField = '';
    
        if (in_array($filter, ['today', 'week', 'month'])) {
            // Set description
            $descriptions = [
                'today' => 'Today',
                'week' => 'This week',
                'month' => 'This month'
            ];
            $this->view->description = $descriptions[$filter];
    
            // Time filter
            $condition = '';
            $bind = [];
    
            switch ($filter) {
                case 'today':
                    $condition = 'hit_time BETWEEN :start: AND :end:';
                    $bind = [
                        'start' => date('Y-m-d 00:00:00'),
                        'end'   => date('Y-m-d 23:59:59')
                    ];
                    break;
                case 'week':
                    $condition = 'hit_time >= :date:';
                    $bind = ['date' => date('Y-m-d H:i:s', strtotime('-7 days'))];
                    break;
                case 'month':
                    $condition = 'hit_time >= :date:';
                    $bind = ['date' => date('Y-m-d H:i:s', strtotime('-30 days'))];
                    break;
            }
    
            // Get chord_ids from chord_hits by views
            $results = $this->modelsManager->createBuilder()
                ->columns('chord_id, COUNT(*) as views')
                ->from(ChordHits::class)
                ->where($condition, $bind)
                ->groupBy('chord_id')
                ->orderBy('views DESC')
                ->limit(100)
                ->getQuery()
                ->execute();
    
            foreach ($results as $row) {
                $chordIds[] = (int) $row->chord_id;
            }
    
            if (empty($chordIds)) {
                $chordIds = [0]; // Prevent empty IN clause
            }
    
            $orderField = 'FIELD(id, ' . implode(',', $chordIds) . ')';
    
            $params = [
                'conditions' => 'id IN ({ids:array})',
                'bind' => ['ids' => $chordIds],
                'order' => $orderField
            ];
        } else {
            // All Time: fallback to Chord.view
            $params = [
                'order' => 'view DESC'
            ];
        }
    
        // Paginate results
        $paginator = new ModelPaginator([
            'model' => Chord::class,
            'parameters' => $params,
            'limit' => 20,
            'page' => $this->request->get('page', 'int', 1),
        ]);
    
        $this->view->chords = $paginator->paginate();
    }
 
    public function chordsOfDayAction()
    {
        $this->view->hide_filter_dropdown = true;
        
        $this->tag->prependTitle('New Chords');
        $chord = new ModelPaginator([
            'model' => Chord::class,
            'parameters' => [
                'order' => 'id DESC'
            ],
            'page' => $this->request->get('page') ?? 1,
            'limit' => 20
        ]);
        $this->view->chords = $chord->paginate();
    }
 
    public function topArtistAction()
    {
        $this->tag->prependTitle('Top Artists');
 
        // if (!$this->cache->has('topartists'))
        //     Caching::topArtists();
 
        // $top_artists = $this->cache->get('topartists');
 
        $top_artists = Artist::find([
            'order' => 'view DESC'
        ]);
 
        $page = $this->request->get('page') ?? 1;
 
        $paginated = $this->cache->get('paginated-top-artist-' . $page);
 
        if ($paginated == null) {
            // $paginator = new ArrayPaginator([
            //     'data' => $top_artists,
            //     'limit' => 39,
            //     'page' => $this->request->get('page') ?? 1
            // ]);
 
            $paginator = new ModelPaginator([
                'model' => Artist::class,
                'parameters' => [
                    'order' => 'view DESC'
                ],
                'limit' => 39,
                'page' => $page
            ]);
 
            $paginated = $paginator->paginate();
        }
        $this->view->artists = $paginated;
    }
 
    public function newestArtistsAction()
    {
        $this->tag->prependTitle('New Artists');
        $page = $this->request->get('page') ?? 1;
        $paginated = $this->request->get('paginated-newest-artist-' . $page);
 
        if ($paginated == null) {
            $paginator = new ModelPaginator([
                'model' => Artist::class,
                'parameters' => [
                    'order' => 'id DESC'
                ],
                'limit' => 27,
                'page' => $this->request->get('page') ?? 1
            ]);
 
            $paginated = $paginator->paginate();
        }
        $this->view->artists = $paginated;
    }
 
    public function chordAction()
    {
        $this->assets->addJs('js/chord.js');
    
        $slug = $this->dispatcher->getParam('slug');
        $initial = $this->dispatcher->getParam('initial');
    
        if ($slug == null) {
            return $this->response->redirect('404');
        }
    
        $findCapital = preg_match('/[A-Z]/', $slug);
        $initialCapital = preg_match('/[A-Z]/', $initial);
    
        if ($findCapital == 1 || $initialCapital == 1) {
            $this->tag->prependTitle('Page Not Found');
            $this->response->setStatusCode(404, 'Page not found!');
            return $this->view->render('index', 'notFound404');
    
            // return $this->response->redirect(strtolower($this->request->get('_url') ?? '/404'));
        }
    
        $chord = Chord::findFirst([
            'slug LIKE :slug:',
            'bind' => [
                'slug' => $slug
            ]
        ]);
        
        if (!$chord) {
            return $this->response->redirect('404');
        }
    
        $chord->featuring = json_encode($chord->featuring);
        $chord->view = $chord->view + 1;
        $chord->save();
        
        $hit = new ChordHits();
        $hit->chord_id = $chord->id;
        $hit->hit_time = date('Y-m-d H:i:s');
        $hit->save();
    
        // $this->tag->prependTitle(strtoupper($chord->title) . ' CHORDS by ' . $chord->Artist->title);
        $this->tag->setTitle(strtoupper($chord->title) . ' Chords by ' . $chord->Artist->title . ' @ Musikord');
        
        $this->view->chord = $chord;
        
        $this->view->pick('index/chord');
        
        $this->view->related_artists = Artist::find([
            'limit' => 12,
            'order' => 'RAND()'
        ]);
    
        $featuringIds = json_decode($chord->featuring, true) ?? [];
        
        $featuringArtists = [];
        if (!empty($featuringIds)) {
            $artists = Artist::query()
                ->inWhere('id', $featuringIds)
                ->execute();
        
            // Optional: Preserve order if needed
            $artistMap = [];
            foreach ($artists as $artist) {
                $artistMap[$artist->id] = $artist;
            }
        
            $featuringArtists = array_map(
                fn($id) => $artistMap[$id] ?? null,
                $featuringIds
            );
        }
        
        $this->view->featuring = $featuringArtists;
        
        $this->view->current_artist = $chord->Artist;
    
        if ($this->session->has('user')) {
            $this->view->is_like_it = (bool)Vote::findFirst([
                'id = :id: AND user = :user:',
                'bind' => [
                    'id' => $chord->id,
                    'user' => $this->session->user->id
                ]
            ]);
        }
    
        $this->view->is_favourite = false;
    
        if ($this->session->has('user')) {
            $this->view->is_favourite = Favourite::count([
                'chord = :chord: AND user = :user:',
                'bind' => [
                    'chord' => $chord->id,
                    'user' => $this->session->user->id
                ]
            ]) > 0;
        }
    
        // $description = substr(strip_tags($chord->chord), 0, 160);
        $cleanChord = strip_tags($chord->chord ?? '');
        $description = mb_substr($cleanChord, 0, 160) . (mb_strlen($cleanChord) > 160 ? '...' : '');
    
        $this->meta['description'] = $description;
        $this->meta['keywords'] = '' . $chord->Artist->title . ' - ' . $chord->title. ' (Chords), chords, ' . $chord->Artist->title . ', Musikord.com, musikord, chord, Cifra, acordes, akkord, accordo, kord, kunci gitar, chord dasar, chord mudah';
        //$this->meta['keywords'] = 'Chord ' . $chord->title . ', Kunci Gitar ' . $chord->title . ', ' . $chord->Artist->title . ', Chord Gitar';
        $this->meta_properties['og:title'] = $this->tag->getTitle();
        $this->meta_properties['og:description'] = $description;
        
        /*
        if ($chord->Artist->artwork) {
            // $this->meta_properties['og:image:url'] = $this->url->get($chord->Artist->artwork);
            $this->meta_properties['og:image'] = $this->url->get($chord->Artist->artwork);
            $this->meta_properties['og:url'] = 'https://www.musikord.com/chords/' . $initial . '/' . $slug . '.html';
            $this->meta_properties['og:type'] = 'article';
            $this->meta_properties['twitter:card'] = 'summary_large_image';
            $this->meta_properties['twitter:site'] = $this->url->get();
            $this->meta_properties['twitter:creator'] = 'Musikord';
            $this->meta_properties['twitter:image'] = $this->url->get($chord->Artist->artwork);
            
            if ($chord->lyric != null || $chord->lyric != '') {
                $this->meta_properties['twitter:description'] = substr(strip_tags($chord->chord), 0, 160) . ' ... (selanjutnya di Musikord)';
            }
        }*/
        
        if ($chord->Artist->artwork) {
            $imageUrl = $this->url->get($chord->Artist->artwork);
            $pageUrl = 'https://www.musikord.com/chords/' . $initial . '/' . $slug . '.html';
        
            $this->meta_properties['og:image'] = $imageUrl;
            $this->meta_properties['og:url'] = $pageUrl;
            $this->meta_properties['og:type'] = 'article';
        
            $this->meta_properties['twitter:card'] = 'summary_large_image';
            $this->meta_properties['twitter:site'] = '@Musikord';
            $this->meta_properties['twitter:creator'] = '@Musikord';
            $this->meta_properties['twitter:image'] = $imageUrl;
        
            // Description (prioritize lyrics or use chords if needed)
            $description = '';
            if (!empty($chord->lyric)) {
                $description = strip_tags($chord->lyric);
            } elseif (!empty($chord->chord)) {
                $description = strip_tags($chord->chord);
            }
        
            // Limit to 160 characters and add suffix
            if ($description) {
                $this->meta_properties['twitter:description'] = mb_substr($description, 0, 160) . ' ... (selengkapnya di Musikord)';
                $this->meta_properties['og:description'] = mb_substr($description, 0, 160) . ' ... (selengkapnya di Musikord)';
            }
        }
    
        $this->view->meta = $this->meta;
        $this->view->meta_property = $this->meta_properties;
        // $this->view->canonical_url = 'chords/' . substr($chord->slug, 0, 1) . '/' . $chord->slug . '.html';
        $this->view->canonical_url = 'https://www.musikord.com/chords/' . strtolower(substr($chord->slug, 0, 1)) . '/' . $chord->slug . '.html';
    }
 
    public function chordsAction()
    {
        $this->tag->prependTitle('Top Chords');
        $page = $this->request->get('page') ?? 1;
 
        $query = $this->modelsManager->createBuilder();
        
        $query
            ->addFrom(Chord::class, 'c')
            ->columns(['c.id', 'SUM(v.value) rate'])
            ->join(Vote::class, 'v.content = c.id AND v.division = "chords"', 'v')
            ->groupBy('c.id')
            ->where('c.chord IS NOT NULL AND c.chord != ""')
            ->orderBy('rate DESC');
 
        $chords = new QueryPaginator([
            'builder' => $query,
            'limit' => 20,
            'page' => $page
        ]);
 
        $paginated = json_decode(json_encode($chords->paginate()));
 
        $items = array_map(
            fn($item) => Chord::findFirst([
                'id = :id:',
                'bind' => [
                    'id' => $item->id
                ]
            ]),
            $paginated->items
        );
 
        $paginated->items = $items;
 
        $this->view->chords = $paginated;
    }
 
    public function playlistAction()
    {
        $this->tag->prependTitle('Playlist');
 
        $playlist = new ModelPaginator([
            'model' => Playlist::class,
            'parameters' => [
                'order' => 'id DESC'
            ],
            'limit' => 12,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        $this->view->playlist = $playlist->paginate();
 
        $this->view->featured_playlist = Playlist::find([
            'order' => 'view DESC',
            'limit' => 10
        ]);
 
        $this->meta['description'] = 'Playlist Musikord';
        $this->meta_properties['og:title'] = 'Playlist Musikord';
        $this->meta_properties['og:description'] = 'Daftar chord pilihan';
 
        $this->view->meta = $this->meta;
        $this->view->meta_property = $this->meta_properties;
    }
 
    public function viewPlaylistAction()
    {
        $slug = $this->dispatcher->getParam('slug');
    
        $playlist = Playlist::findFirst([
            'slug = :slug:',
            'bind' => ['slug' => $slug]
        ]);
    
        if (!$playlist) {
            return $this->response->redirect('404');
        }
    
        $playlist->view++;
        $playlist->save();
    
        $title = 'Playlist: ' . $playlist->title . ' @ Musikord';
        $description = 'Discover handpicked guitar chord playlists curated by Musikord — perfect for practice, jamming, or discovering new music.';
        $imageUrl = $playlist->image ? $this->url->get($playlist->image) : $this->url->get('images/bg_default.jpg');
        $pageUrl = 'https://www.musikord.com/playlist/' . $slug . '.html';
    
        $this->tag->setTitle($title);
        $this->meta['description'] = $description;
        $this->meta['keywords'] = $playlist->title . ', guitar playlist, chord, Musikord';
    
        $this->meta_properties = [
            'og:title' => $title,
            'og:description' => $description,
            'og:image' => $imageUrl,
            'og:url' => $pageUrl,
            'og:type' => 'article',
            'twitter:card' => 'summary_large_image',
            'twitter:site' => '@Musikord',
            'twitter:creator' => '@Musikord',
            'twitter:image' => $imageUrl,
            'twitter:description' => $description
        ];
    
        $this->view->playlist = $playlist;
        $this->view->related_artists = Artist::find([
            'limit' => 12,
            'order' => 'RAND()'
        ]);
        $this->view->meta = $this->meta;
        $this->view->meta_property = $this->meta_properties;
    }
 
    public function newsAction()
    {
        $this->tag->prependTitle('News');
 
        $news = new ModelPaginator([
            'model' => News::class,
            'parameters' => [
                'order' => 'id DESC'
            ],
            'limit' => 12,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        $this->view->news = $news->paginate();
        
        $this->view->news_slider = News::find([
            'order' => 'view DESC',
            'limit' => 5
        ]);
 
        $this->view->news_category = NewsCategory::get();
    }
 
    public function newsByCategoryAction()
    {
        $category = $this->dispatcher->getParam('category');
        $this->tag->prependTitle('Berita ' . NewsCategory::view($category)['title']);
 
        $news = new ModelPaginator([
            'model' => News::class,
            'parameters' => [
                'category = :category:',
                'bind' => [
                    'category' => $category
                ],
                'order' => 'id DESC'
            ],
            'limit' => 12,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        $this->view->news = $news->paginate();
        
        $this->view->news_slider = News::find([
            'order' => 'view DESC',
            'limit' => 5
        ]);
 
        $this->view->news_category = NewsCategory::get();
    }
 
    public function readNewsAction()
    {
        $news = News::findFirst([
            'category = :category: AND slug = :slug:',
            'bind' => [
                'category' => $this->dispatcher->getParam('category'),
                'slug' => $this->dispatcher->getParam('slug')
            ]
        ]);
    
        if (!$news) {
            return $this->response->redirect('404');
        }
    
        $news->view++;
        $news->save();
    
        $title = $news->title . ' @ Musikord';
        $description = mb_substr(strip_tags($news->content ?? ''), 0, 160);
        $imageUrl = $news->image ? $this->url->get($news->image) : $this->url->get('/public/images/bg_default.jpg');
        $pageUrl = 'https://www.musikord.com/news/' . $news->category . '/' . $news->slug . '.html';
    
        $this->tag->setTitle($title);
        $this->meta['description'] = $description;
        $this->meta['keywords'] = $news->title . ', music news, popular artist, latest celebrity news, latest gossips';
    
        $this->meta_properties = [
            'og:title' => $title,
            'og:description' => $description,
            'og:image' => $imageUrl,
            'og:url' => $pageUrl,
            'og:type' => 'article',
            'twitter:card' => 'summary_large_image',
            'twitter:site' => '@Musikord',
            'twitter:creator' => '@Musikord',
            'twitter:image' => $imageUrl,
            'twitter:description' => $description
        ];
    
        $this->view->news = $news;
        $this->view->news_category = NewsCategory::get();
        $this->view->meta = $this->meta;
        $this->view->meta_property = $this->meta_properties;
    }
 
    public function videoAction()
    {
        $this->tag->prependTitle('Video');
        
        $video = new ModelPaginator([
            'model' => Video::class,
            'parameters' => [
                'order' => 'id DESC'
            ],
            'limit' => 21,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        $this->view->video = $video->paginate();
        
        $this->view->video_slider = Video::find([
            'order' => 'view DESC',
            'limit' => 5
        ]);
 
        $this->view->video_category = VideoCategory::get();
    }
 
    public function videoByCategoryAction()
    {
        $category = $this->dispatcher->getParam('category');
        $this->tag->prependTitle('Video ' . VideoCategory::view($category)['title']);
        
        $video = new ModelPaginator([
            'model' => Video::class,
            'parameters' => [
                'category = :category:',
                'bind' => [
                    'category' => $category
                ],
                'order' => 'id DESC'
            ],
            'limit' => 12,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        $this->view->video = $video->paginate();
        
        $this->view->video_slider = Video::find([
            'order' => 'view DESC',
            'limit' => 5
        ]);
 
        $this->view->video_category = VideoCategory::get();
    }
 
    public function viewVideoAction()
    {
        $video = Video::findFirst([
            'category = :category: AND slug = :slug:',
            'bind' => [
                'category' => $this->dispatcher->getParam('category'),
                'slug' => $this->dispatcher->getParam('slug')
            ]
        ]);
        
        foreach ($videos as $video) {
            $video->time_ago = Carbon::parse($video->create_time)->diffForHumans(); 
            // or with native PHP:
            // $video->time_ago = (new \DateTime($video->create_time))->diff(new \DateTime())->format('%a days ago');
        }
    
        if (!$video) {
            return $this->response->redirect('404');
        }
    
        $video->view++;
        $video->save();
    
        $this->tag->setTitle($video->title . ' @ Musikord');
    
        $description = mb_substr(strip_tags($video->content ?? ''), 0, 160);
        $imageUrl = $video->url ? 'https://i.ytimg.com/vi/' . $video->url . '/hqdefault.jpg' : '/public/images/bg_default.jpg';
        $pageUrl = 'https://www.musikord.com/video/' . $video->category . '/' . $video->slug . '.html';
    
        $this->meta['description'] = $description;
        $this->meta['keywords'] = $video->title . ', video music, guitar tutorial, Musikord';
    
        $this->meta_properties = [
            'og:title' => $video->title . ' @ Musikord',
            'og:description' => $description,
            'og:image' => $imageUrl,
            'og:url' => $pageUrl,
            'og:type' => 'video.other',
            'twitter:card' => 'summary_large_image',
            'twitter:site' => '@Musikord',
            'twitter:creator' => '@Musikord',
            'twitter:image' => $imageUrl,
            'twitter:description' => $description
        ];
    
        $this->view->video = $video;
        $this->view->video_category = VideoCategory::get();
        $this->view->meta = $this->meta;
        $this->view->meta_property = $this->meta_properties;
    }
 
    public function contributeAction()
    {
        $this->tag->prependTitle('Kontribusi');
 
        $chord = new ModelPaginator([
            'model' => Chord::class,
            'parameters' => [
                'chord = "" OR chord IS NULL',
                'order' => 'id DESC'
            ],
            'page' => $this->request->get('page') ?? 1,
            'limit' => 20
        ]);
        
        $this->view->chords = $chord->paginate();
        
        $this->view->most_viewed_chords = Chord::find([
            'chord = "" OR chord IS NULL',
            'order' => 'view DESC',
            'limit' => 10
        ]);
    }
 
    public function pageAction()
    {
        $slug = $this->dispatcher->getParam('slug');
        $page = Page::findFirstBySlug($slug);
        $this->tag->prependTitle($page->title);
        $this->view->page = $page;
    }
 
    public function verifiedAction()
    {
        $this->tag->prependTitle('Get Verified');
    }
 
    public function sanitySearchAction()
    {
        $query = $this->request->get('s');
        $trimmed_query = trim($query);
        if (strlen($trimmed_query) < 1) {
            $this->response->redirect($_SERVER['HTTP_REFERER']);
        } else {
            $this->response->redirect('search/' . $trimmed_query);
        }
    }
 
    public function searchAction($query)
    {
        $this->tag->prependTitle('Search');
        $query = urldecode($query);
        
        $paginator = new ModelPaginator([
            'model' => Artist::class,
            'parameters' => [
                'title LIKE :find:',
                'bind' => [
                    'find' => '%' . $query . '%'
                ]
            ],
            'limit' => 20,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        $paginated = $paginator->paginate();
 
        $this->view->artists = $paginated;
        $this->view->find = $query;
    }
 
    public function requestAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=request');
        }
    
        // Handle POST (form submission)
        if ($this->request->isPost()) {
            $request = new UserRequest();
            $request->user = $this->session->get('user')->id;
            $request->title = $this->request->getPost('title', 'string');
            $request->artist = $this->request->getPost('artist');
    
            if (!$request->save()) {
                foreach ($request->getMessages() as $msg) {
                    $this->flash->error($msg->getMessage());
                }
    
                // Optional: preserve user input
                $this->tag->setDefaults([
                    'title' => $this->request->getPost('title'),
                    'artist' => $this->request->getPost('artist')
                ]);
    
            } else {
                // Award +1 point for successful request
                $point = new Point();
                $point->user = $this->session->get('user')->id;
                $point->point = 1;
                $point->category = 'request_chord';
                $point->ref_id = $request->id;
                $point->save();
    
                $this->flash->success('Request submitted successfully!');
                return $this->response->redirect('requests'); // Adjust to your request listing
            }
        }
    
        // Assets and form rendering for GET request
        $this->session->set('userinput', time());
    
        $this->assets->collection('headerAssets')
            ->addCss('https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2.min.css');
    
        $this->tag->prependTitle('Request Chord');
        $this->view->artists = Artist::find(['order' => 'title ASC']);
    }
 
    public function submitRequestEntryAction()
    {
        if ($this->request->get('captcha') != $this->session->captcha) {
            $this->flash->error('Captcha not match');
            return $this->response->redirect('request');
        }
 
        if (!$this->session->has('userinput')) {
            return $this->response->redirect('request');
        }
 
        $this->session->remove('userinput');
        $this->session->remove('captcha');
 
        $user = $this->session->get('user');
        
        $request = new UserRequest();
        $request->fullname = $user->name;
        $request->email = $user->email;
        $request->title = $this->request->get('title');
        $request->artist = $this->request->get('artist');
        $save_request = $request->save();
 
        $point = new Point();
 
        $point->user = $user->id;
        $point->point = 1;
        $point->category = 'chord_request';
        $point->ref_id = $request->id;
        
        $point->save();
 
        array_map(fn($err) => $this->flash->error($err->getMessage()), $request->getMessages());
 
        if ($save_request) {
            $this->flash->success('Request saved');
        }
 
        return $this->response->redirect('request');
    }
 
    public function submitEntryAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=submit');
        }
 
        if ($this->request->get('captcha') != $this->session->captcha) {
            return $this->response->redirect('submit?' . http_build_query([
                'content'
            ]));
        }
 
        $this->session->remove('captcha');
 
        $user_chord = new UserChord();
        $user_chord->user = $this->session->user->id;
        $user_chord->title = $this->request->get('title', 'string');
        $user_chord->content = $this->request->get('content', 'striptags');
        $user_chord->artist = $this->request->get('artist');
        $save_chord = $user_chord->save();
 
        if (!$save_chord) {
            array_map(fn($err) => $this->flash->error($err->getMessage()), $user_chord->getMessages());
 
            return $this->response->redirect('submit?failed=true&' . http_build_query([
                'title' => $this->request->get('title'),
                'content' => $this->request->get('content'),
                'artist' => $this->request->get('artist')
            ]));
        }
 
        $point = new Point();
 
        $point->user = $this->session->get('user')->id;
        $point->point = 1;
        $point->category = 'submit_chord';
        $point->ref_id = $user_chord->id;
 
        $point->save();
 
        $this->flash->success('Chord saved');
        return $this->response->redirect('preview/' . $user_chord->id);
    }
 
    public function submitAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=submit');
        }
 
        $this->footerAssets
            ->addCss('https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css', false)
            ->addJs('https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js', false)
            ->addJs('js/submitChord.js');
 
        if ($this->request->get('failed') == 'true') {
            $this->tag->setDefaults([
                'title' => $this->request->get('title'),
                'content' => $this->request->get('content'),
                'artist' => $this->request->get('artist')
            ]);
 
            $this->view->artist = $this->request->get('artist');
        }
 
        $this->session->set('userinput', time());
 
        $this->tag->prependTitle('Submit Chord');
        $this->view->artists = Artist::find(['order' => 'title ASC']);
    }
 
    public function submitExistingSongAction($slug)
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=submit/' . $slug);
        }
 
        $chord = Chord::findFirst([
            'slug = :slug:',
            'bind' => [
                'slug' => $slug
            ]
        ]);
 
        if (!$chord) {
            return $this->response->redirect('404');
        }
 
        $submitted = UserChord::findFirst([
            'chord = :chord: AND user = :user:',
            'bind' => [
                'chord' => $chord->id,
                'user' => $this->session->user->id
            ]
        ]);
 
        if ($submitted) {
            return $this->response->redirect('edit-chord/' . $submitted->id);
        }
 
        $this->session->set('userinput', time());
        
        $this->tag->prependTitle('Submit Chord ' . $chord->title);
        $this->tag->setDefault('content', $this->request->get('content'));
 
        $this->view->chord = $chord;
    }
 
    public function submitEntryExistingSongAction()
    {
        $slug = $this->dispatcher->getParam('slug');
    
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=submit/' . $slug);
        }
    
        $chord = Chord::findFirst([
            'slug = :slug:',
            'bind' => [
                'slug' => $slug
            ]
        ]);
    
        if (!$chord) {
            return $this->response->redirect('404');
        }
    
        $user_chord = new UserChord();
        $user_chord->user = $this->session->user->id;
        $user_chord->artist = $chord->artist;
        $user_chord->chord = $chord->id;
        $user_chord->title = $chord->title;
        $user_chord->content = $this->request->get('content', 'striptags');
        $save = $user_chord->save();
    
        if (!$save) {
            array_map(
                fn($err) => $this->flash->error('error', $err->getMessage()),
                $user_chord->getMessages()
            );
    
            return $this->response->redirect('submit/' . $slug . '?content=' . $this->request->get('content', 'striptags'));
        }
    
        // ✅ Award 3 points for submitting to existing chord
        $point = new Point();
        $point->user = $this->session->get('user')->id;
        $point->point = 3;
        $point->category = 'submit_existing_chord';
        $point->ref_id = $user_chord->id;
        $point->create_time = date('Y-m-d H:i:s'); // Optional if not auto-set in DB
        $point->save();
    
        return $this->response->redirect('preview/' . $user_chord->id);
    }
 
    public function captchaAction()
    {
        $this->response->setHeader('Content-type', 'image/jpg');
        
        $properties = (object)[
            'width' => 100,
            'height' => 30
        ];
 
        if (!$this->session->has('userinput')) {
            # Cached default image cropping
            $image = $this->cache->get('defaultcaptchaimg');
 
            if ($image == null) {
                $image = new Image(BASE_PATH . '/public/images/bg_default1.jpg');
                $image->crop($properties->width, $properties->height, 0, 0);
                $this->cache->save('defaultcaptchaimg', $image->render(), 60 * 60 * 24 * 7);
            }
 
            return $this->response->setContent($image);
        }
 
        $token = substr(uniqid(), 7, 6);
        $this->session->set('captcha', $token);
        $image = new Image(BASE_PATH . '/public/images/bg_default.jpg');
        $image->crop($properties->width, $properties->height, 0, 0);
        $image->text($token, 10, 10, 100, '#000000');
 
        return $this
            ->response
            ->setContent($image->render('jpg', 8));
    }
 
    public function featuredAction()
    {
        $this->tag->prependTitle('Get Featured');
    }
 
    public function profileAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth');
        }
        
        $this->tag->prependTitle('Edit Profil');
        
        $this->tag->setDefaults([
            'name' => $this->session->user->name,
            'email' => $this->session->user->email,
            'fullname' => $this->session->user->meta->fullname,
            'bio' => $this->session->user->meta->bio,
            'facebook' => $this->session->user->meta->facebook,
            'twitter' => $this->session->user->meta->twitter,
            'website' => $this->session->user->meta->website
        ]);
    }
 
    public function updateProfileAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth');
        }
    
        $user = User::findFirst([
            'conditions' => 'id = :id:',
            'bind' => ['id' => $this->session->user->id]
        ]);
    
        if (!$user) {
            $this->flash->error('User tidak ditemukan');
            return $this->response->redirect('profile');
        }
    
        // Password update (only if both fields are filled & match)
        $password = $this->request->getPost('password', 'trim');
        $passwordRepeat = $this->request->getPost('password-repeat', 'trim');
    
        if ($password && $passwordRepeat && $password === $passwordRepeat) {
            $user->password = password_hash($password, PASSWORD_DEFAULT);
        }
    
        // Meta data update
        $metaData = [
            'fullname' => $this->request->getPost('fullname', 'trim'),
            'bio'      => $this->request->getPost('bio', 'trim'),
            'twitter'  => $this->request->getPost('twitter', 'trim'),
            'facebook' => $this->request->getPost('facebook', 'trim'),
            'website'  => $this->request->getPost('website', 'trim')
        ];
    
        $user->meta = json_encode($metaData);
    
        if ($user->save()) {
            // Refresh session user with latest data
            $updatedUser = User::findFirstById($user->id);
            $this->session->set('user', $updatedUser);
    
            $this->flash->success('Profil berhasil diperbarui');
        } else {
            $this->flash->error('Gagal menyimpan data');
            foreach ($user->getMessages() as $message) {
                $this->flash->error($message);
            }
        }
    
        return $this->response->redirect('profile');
    }
 
    public function addFavouriteAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=' . $this->request->getHTTPReferer());
        }
 
        $favourite = Favourite::findFirst([
            'chord = :chord: AND user = :user:',
            'bind' => [
                'chord' => $this->dispatcher->getParam('id'),
                'user' => $this->session->user->id
            ]
        ]);
 
        if (!$favourite) {
            $new_favourite = new Favourite();
            $new_favourite->user = $this->session->user->id;
            $new_favourite->chord = $this->dispatcher->getParam('id');
            $new_favourite->save();
        } else {
            $favourite->delete();
        }
 
        return $this->response->redirect($this->request->getHTTPReferer());
    }
 
    public function favouriteAction()
    {
        $this->tag->prependTitle('Favourite');
 
        $chord = new Phalcon\Paginator\Adapter\Model([
            'model' => Favourite::class,
            'parameters' => [
                'user = :user:',
                'bind' => [
                    'user' => $this->session->user->id
                ]
            ],
            'page' => $this->request->get('page') ?? 1,
            'limit' => 20
        ]);
 
        $paginated = json_decode(json_encode($chord->paginate()));
 
        $paginated->items = array_map(
            fn($data) => Chord::findFirst([
                'id = :id:',
                'bind' => [
                    'id' => $data->chord
                ]
            ]),
            $paginated->items
        );
 
        $this->view->chords = $paginated;
    }
 
    public function myChordAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=my-chord');
        }
 
        $this->tag->prependTitle('Submitted');
        
        $submitted = new ModelPaginator([
            'model' => UserChord::class,
            'parameters' => [
                'user = :user_id:',
                'bind' => [
                    'user_id' => $this->session->user->id
                ]
            ],
            'page' => $this->request->get('page') ?? 1,
            'limit' => 20
        ]);
 
        $this->view->chord = $submitted->paginate();
    }
 
    public function previewChordAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth');
        }
 
        $user_chord = UserChord::findFirst([
            'id = :id: AND user = :user:',
            'bind' => [
                'id' => $this->dispatcher->getParam('id'),
                'user' => $this->session->user->id
            ]
        ]);
 
        if (!$user_chord) {
            return $this->response->redirect('404');
        }
 
        if ($user_chord->verified == UserChord::VERIFIED) {
            $chord = $user_chord->Chord;
            return $this->response->redirect('chords/' . substr($chord->slug, 0, 1) . '/' . $chord->slug . '.html');
        }
 
        $this->view->chord = $user_chord;
        $this->view->related_artists = Artist::find([
            'limit' => 12,
            'order' => 'RAND()'
        ]);
    }
 
    public function editChordAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth');
        }
 
        $user_chord = UserChord::findFirst([
            'id = :id: AND user = :user:',
            'bind' => [
                'id' => $this->dispatcher->getParam('id'),
                'user' => $this->session->user->id
            ]
        ]);
 
        if (!$user_chord) {
            return $this->response->redirect('404');
        }
 
        $this->tag->setDefaults([
            'id' => $user_chord->id,
            'chord' => $user_chord->chord,
            'artist' => $user_chord->artist,
            'title' => $user_chord->title,
            'content' => $user_chord->content
        ]);
 
        $this->view->artists = Artist::find();
        $this->view->chord = $user_chord;
        $this->view->custom_chord = $user_chord->chord != null;
    }
 
    public function submitEditChordAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth');
        }
 
        $user_chord = UserChord::findFirst([
            'id = :id: AND user = :user:',
            'bind' => [
                'id' => $this->request->get('id'),
                'user' => $this->session->user->id
            ]
        ]);
 
        if (!$user_chord) {
            return $this->response->redirect('404');
        }
 
        if ($user_chord->chord == null) {
            $user_chord->artist = $this->request->get('artist');
            $user_chord->title = $this->request->get('title');
        }
 
        $user_chord->content = $this->request->get('content');
 
        $save_user_chord = $user_chord->save();
 
        if (!$save_user_chord) {
            array_map(fn($err) => $this->flash->error($err->getMessage()), $user_chord->getMessages());
            return $this->response->redirect($this->request->getHTTPReferer());
        }
 
        $this->flash->notice('Chord saved');
        return $this->response->redirect('preview/' . $user_chord->id);
    }
 
    public function badgeAction()
    {
        $this->tag->prependTitle('Badge');
    }
 
    public function supportAction()
    {
        $this->tag->prependTitle('Support');
    }
 
    public function userAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=' . $this->request->get('_url'));
        }
        
        $this->tag->prependTitle('User List');
 
        $user = new ModelPaginator([
            'model' => User::class,
            'parameters' => [
                'status = :status:',
                'bind' => [
                    'status' => User::ACTIVE
                ]
            ],
            'limit' => 20,
            'page' => $this->request->get('page') ?? 1
        ]);
 
        $this->view->user = $user->paginate();
    }
 
    public function showUserAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=' . $this->request->get('_url'));
        }
        
        $user = User::findFirst([
            'name = :name: AND status = :status:',
            'bind' => [
                'name' => $this->dispatcher->getParam('name'),
                'status' => User::ACTIVE
            ]
        ]);
 
        if (!$user) {
            return $this->response->redirect('404');
        }
 
        $this->view->user = $user;
    }
 
    public function postCommentAction()
    {
        if (!$this->session->has('user')) {
            return $this->response->redirect('auth?r=' . $this->request->getHTTPReferer());
        }
 
        $comment = new Comment();
        $comment->uri = $this->request->get('path', 'string');
        $comment->user = $this->session->user->id;
        $comment->text = $this->request->get('comment', 'striptags');
        $comment->verified = $this->session->user->privilege == User::ADMIN ? Comment::VERIFIED : Comment::UNVERIFIED;
        $saved_comment = $comment->save();
 
        array_map(
            fn($err) => $this->flash->error($err->getMessage()),
            $comment->getMessages()
        );
 
        if ($saved_comment && $this->session->user->privilege != User::ADMIN) {
            $this->flash->success('Thank you for your comment, your comment awaiting approval from our admin.');
        }
 
        return $this->response->redirect($this->request->getHTTPReferer() . '#comment-container');
    }
 
    public function sitemapAction()
    {
        $this->view->disable();
    
        // Count all entries
        $chordTotal    = Chord::count();
        $videoTotal    = Video::count();
        $artistTotal   = Artist::count();
        $playlistTotal = Playlist::count();
    
        $limit = 300; // per sitemap file
        $baseUrl = rtrim($this->url->getBaseUri(), '/');
    
        $xml = new \XMLWriter();
        $xml->openMemory();
        $xml->startDocument('1.0', 'UTF-8');
        $xml->setIndent(true);
    
        // Start <sitemapindex>
        $xml->startElement('sitemapindex');
        $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
    
        // Add static sitemap files
        $staticSitemaps = [
            'main-sitemap.xml',
            'genre-sitemap.xml'
        ];
    
        foreach ($staticSitemaps as $file) {
            $xml->startElement('sitemap');
            $xml->writeElement('loc', $baseUrl . '/' . $file);
            $xml->writeElement('lastmod', date('Y-m-d'));
            $xml->endElement();
        }
    
        // Add segmented dynamic sitemaps
        $types = [
            'chord'    => $chordTotal,
            'video'    => $videoTotal,
            'artist'   => $artistTotal,
            'playlist' => $playlistTotal
        ];
    
        foreach ($types as $type => $total) {
            $pages = ceil($total / $limit);
            for ($i = 1; $i <= $pages; $i++) {
                $xml->startElement('sitemap');
                $xml->writeElement('loc', $baseUrl . '/sub-sitemap-' . $type . '-' . $i . '.xml');
                $xml->writeElement('lastmod', date('Y-m-d'));
                $xml->endElement();
            }
        }
    
        // Close <sitemapindex>
        $xml->endElement();
        $xml->endDocument();
    
        return $this->response
            ->setHeader('Content-Type', 'application/xml')
            ->setContent($xml->outputMemory())
            ->send();
    }
 
    public function mainSitemapAction() {
        $xml = new XMLWriter();
        $xml->openMemory();
        $xml->startDocument('1.0', 'utf-8');
        $xml->setIndent(true);
        $xml->startElement('urlset');
        $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
        $xml->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
        $xml->writeAttribute('xsi:schemaLocation', 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd');
 
        $data = [
            [
                'text' => $this->url->get(),
                'timestamp' => '2020-10-27T13:23:57+07:00'
            ],
            [
                'text' => $this->url->get('chords/'),
                'timestamp' => '2020-10-27T13:23:57+07:00'
            ]
        ];
 
        foreach ($data as $row) {
            $xml->startElement('url');
            $xml->startElement('loc');
            $xml->text($row['text']);
            $xml->endElement();
            
            $xml->startElement('lastmod');
            $xml->text(date('Y-m-d\TH:i:s+07:00', strtotime($row['timestamp'])));
            $xml->endElement();
    
            $xml->startElement('changefreq');
            $xml->text('daily');
            $xml->endElement();
    
            $xml->startElement('priority');
            $xml->text('0.9');
            $xml->endElement();
            
            $xml->endElement();
        }
 
        $xml->endElement();
        $xml->endDocument();
 
        return $this->response
            ->setHeader('Content-Type', 'text/xml')
            ->setContent($xml->outputMemory())
            ->send();
    }
 
    public function genreSitemapAction() {
        $xml = new XMLWriter();
        $xml->openMemory();
        $xml->startDocument('1.0', 'utf-8');
        $xml->setIndent(true);
        $xml->startElement('urlset');
        $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
        $xml->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
        $xml->writeAttribute('xsi:schemaLocation', 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd');
 
        foreach (GenreList::get() as $row) {
            $xml->startElement('url');
            $xml->startElement('loc');
            $xml->text($this->url->get('artists/genre/' . strtolower($row['slug'])));
            $xml->endElement();
            
            $xml->startElement('lastmod');
            $xml->text('2020-10-27T13:23:57+07:00');
            $xml->endElement();
    
            $xml->startElement('changefreq');
            $xml->text('daily');
            $xml->endElement();
    
            $xml->startElement('priority');
            $xml->text('0.9');
            $xml->endElement();
            
            $xml->endElement();
        }
 
        $xml->endElement();
        $xml->endDocument();
 
        return $this->response
            ->setHeader('Content-Type', 'text/xml')
            ->setContent($xml->outputMemory())
            ->send();
    }
 
    public function subSitemapAction($segment, $page) {
        $xml = new XMLWriter();
        $xml->openMemory();
        $xml->startDocument('1.0', 'utf-8');
        $xml->setIndent(true);
        $xml->startElement('urlset');
        $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
        $xml->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
        $xml->writeAttribute('xsi:schemaLocation', 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd');
 
        $data = [];
 
        $query = [
            'offset' => ($page - 1) * 300,
            'limit' => 300
        ];
 
        if ($segment === 'chord') {
            $chordList = Chord::find($query);
 
            foreach ($chordList as $row) {
                $data[] = [
                    'text' => $this->url->get('chords/' . strtolower(substr($row->slug, 0, 1)) . '/' . strtolower($row->slug) . '.html'),
                    'timestamp' => $row->create_time
                ];
            }
        }
 
        if ($segment === 'video') {
            $videoList = Video::find($query);
 
            foreach ($videoList as $row) {
                $data[] = [
                    'text' => $this->url->get('video/video-clip/' . strtolower(substr($row->slug, 0, 1)) . '/' . strtolower($row->slug) . '.html'),
                    'timestamp' => $row->create_time
                ];
            }
        }
 
        if ($segment === 'artist') {
            $artistList = Artist::find($query);
 
            foreach ($artistList as $row) {
                $data[] = [
                    'text' => $this->url->get('artists/' . strtolower(substr($row->slug, 0, 1)) . '/' . strtolower($row->slug) . '.html'),
                    'timestamp' => $row->create_time
                ];
            }
        }
 
        if ($segment === 'playlist') {
            $playlistList = Playlist::find($query);
 
            foreach ($playlistList as $row) {
                $data[] = [
                    'text' => $this->url->get('playlist/' . strtolower($row->slug) . '.html'),
                    'timestamp' => $row->create_time
                ];
            }
        }
 
        foreach ($data as $row) {
            $xml->startElement('url');
            $xml->startElement('loc');
            $xml->text($row['text']);
            $xml->endElement();
            
            $xml->startElement('lastmod');
            $xml->text(date('Y-m-d\TH:i:s+07:00', strtotime($row['timestamp'])));
            $xml->endElement();
    
            $xml->startElement('changefreq');
            $xml->text('daily');
            $xml->endElement();
    
            $xml->startElement('priority');
            $xml->text('0.9');
            $xml->endElement();
            
            $xml->endElement();
        }
 
        $xml->endElement();
        $xml->endDocument();
 
        return $this->response
            ->setHeader('Content-Type', 'text/xml')
            ->setContent($xml->outputMemory())
            ->send();
    }
 
    public function redirectToNotFound404Action()
    {
        return $this->response->redirect('404');
    }
 
    public function notFound404Action()
    {
        $this->tag->prependTitle('Page Not Found');
        $this->response->setStatusCode(404, 'Page not found!');
    }
}
#11IndexController->chordAction
#12Phalcon\Dispatcher\AbstractDispatcher->callActionMethod
#13Phalcon\Dispatcher\AbstractDispatcher->dispatch
#14Phalcon\Mvc\Application->handle
/var/www/html/public/index.php (29)
<?php
define('BASE_PATH', dirname(__DIR__));
define('APP_PATH', BASE_PATH . '/app');
 
date_default_timezone_set('Asia/Jakarta');
 
error_reporting(E_ALL);
ini_set('display_errors', 1);
 
require(BASE_PATH . '/vendor/autoload.php');
 
use Phalcon\Di\FactoryDefault;
use Phalcon\Debug;
 
// Enable Phalcon debug screen
$debug = new Debug();
$debug->listen();
 
try {
    $di = new FactoryDefault();
 
    include APP_PATH . '/config/router.php';
    include APP_PATH . '/config/services.php';
 
    $config = $di->getConfig();
    include APP_PATH . '/config/loader.php';
 
    $application = new \Phalcon\Mvc\Application($di);
    echo $application->handle($_SERVER['REQUEST_URI'])->getContent();
 
} catch (\Exception $e) {
    // Temporarily disable redirect for debugging
    // header('location:' . $config->application->baseUri . '404');
    // die($e->getMessage());
 
    echo '<pre>' . $e . '</pre>';
}
KeyValue
_url/chords/c/champagne-supernova.html
KeyValue
USERwww-data
HOME/var/www
HTTP_HOSTwww.musikord.com
HTTP_ACCEPT_ENCODINGgzip, br, zstd, deflate
HTTP_USER_AGENTMozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
HTTP_ACCEPT*/*
REDIRECT_STATUS200
SERVER_NAMEmusikord.com
SERVER_PORT443
SERVER_ADDR10.64.15.165
REMOTE_USER
REMOTE_PORT5426
REMOTE_ADDR216.73.216.246
SERVER_SOFTWAREnginx/1.24.0
GATEWAY_INTERFACECGI/1.1
HTTPSon
REQUEST_SCHEMEhttps
SERVER_PROTOCOLHTTP/1.1
DOCUMENT_ROOT/var/www/html/public
DOCUMENT_URI/index.php
REQUEST_URI/chords/c/champagne-supernova.html
SCRIPT_NAME/index.php
CONTENT_LENGTH
CONTENT_TYPE
REQUEST_METHODGET
QUERY_STRING_url=/chords/c/champagne-supernova.html&
SCRIPT_FILENAME/var/www/html/public/index.php
PATH_INFO
FCGI_ROLERESPONDER
PHP_SELF/index.php
REQUEST_TIME_FLOAT1753023542.4172
REQUEST_TIME1753023542
#Path
0/var/www/html/public/index.php
1/var/www/html/vendor/autoload.php
2/var/www/html/vendor/composer/autoload_real.php
3/var/www/html/vendor/composer/platform_check.php
4/var/www/html/vendor/composer/ClassLoader.php
5/var/www/html/vendor/composer/autoload_static.php
6/var/www/html/vendor/ralouphie/getallheaders/src/getallheaders.php
7/var/www/html/vendor/symfony/deprecation-contracts/function.php
8/var/www/html/vendor/guzzlehttp/guzzle/src/functions_include.php
9/var/www/html/vendor/guzzlehttp/guzzle/src/functions.php
10/var/www/html/vendor/google/apiclient-services/autoload.php
11/var/www/html/vendor/phpseclib/phpseclib/phpseclib/bootstrap.php
12/var/www/html/vendor/google/apiclient/src/aliases.php
13/var/www/html/vendor/google/apiclient/src/Client.php
14/var/www/html/vendor/google/apiclient/src/Service.php
15/var/www/html/vendor/google/apiclient/src/AccessToken/Revoke.php
16/var/www/html/vendor/google/apiclient/src/AccessToken/Verify.php
17/var/www/html/vendor/google/apiclient/src/Model.php
18/var/www/html/vendor/google/apiclient/src/Utils/UriTemplate.php
19/var/www/html/vendor/google/apiclient/src/AuthHandler/Guzzle6AuthHandler.php
20/var/www/html/vendor/google/apiclient/src/AuthHandler/Guzzle7AuthHandler.php
21/var/www/html/vendor/google/apiclient/src/AuthHandler/AuthHandlerFactory.php
22/var/www/html/vendor/google/apiclient/src/Http/Batch.php
23/var/www/html/vendor/google/apiclient/src/Http/MediaFileUpload.php
24/var/www/html/vendor/google/apiclient/src/Http/REST.php
25/var/www/html/vendor/google/apiclient/src/Task/Retryable.php
26/var/www/html/vendor/google/apiclient/src/Task/Exception.php
27/var/www/html/vendor/google/apiclient/src/Exception.php
28/var/www/html/vendor/google/apiclient/src/Task/Runner.php
29/var/www/html/vendor/google/apiclient/src/Collection.php
30/var/www/html/vendor/google/apiclient/src/Service/Exception.php
31/var/www/html/vendor/google/apiclient/src/Service/Resource.php
32/var/www/html/vendor/google/apiclient/src/Task/Composer.php
33/var/www/html/app/config/router.php
34/var/www/html/app/config/services.php
35/var/www/html/app/config/config.php
36/var/www/html/app/config/loader.php
37/var/www/html/cache/_var_www_html_app_views_components_macros_chord-card.volt.php
38/var/www/html/cache/_var_www_html_app_views_components_macros_artist-card.volt.php
39/var/www/html/cache/_var_www_html_app_views_components_macros_gravatar.volt.php
40/var/www/html/app/controllers/IndexController.php
41/var/www/html/app/controllers/ControllerBase.php
42/var/www/html/app/models/Chord.php
43/var/www/html/cache/metadata/map-chord.php
44/var/www/html/cache/metadata/meta-chord-chord.php
Memory
Usage2097152