├── .editorconfig ├── .env ├── .env.test ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── assets ├── js │ ├── about.js │ ├── main.js │ ├── search.js │ ├── show.js │ ├── trends.js │ ├── twig │ │ └── result.html.twig │ └── vendor │ │ ├── iphone-style.js │ │ ├── jquery-3.6.0.min.js │ │ ├── raphael.min.js │ │ ├── tablesort.js │ │ └── twig.min.js ├── scss │ ├── partials │ │ ├── embed.scss │ │ ├── general.scss │ │ ├── iphone-style.scss │ │ ├── media-queries.scss │ │ ├── reset.scss │ │ └── style.scss │ └── styles.scss ├── styles │ └── app.css └── widget │ ├── widget-overlay.scss │ ├── widget.js │ └── widget.scss ├── bin ├── console └── phpunit ├── codeception.yml ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── asset_mapper.yaml │ ├── cache.yaml │ ├── debug.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── league_oauth2_server.yaml │ ├── mailer.yaml │ ├── monolog.yaml │ ├── nelmio_cors.yaml │ ├── nyholm_psr7.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── validator.yaml │ └── web_profiler.yaml ├── preload.php ├── routes.yaml ├── routes │ ├── framework.yaml │ ├── league_oauth2_server.yaml │ ├── security.yaml │ └── web_profiler.yaml └── services.yaml ├── deploy ├── Caddyfile ├── deploy.yml ├── nginx.conf └── vars.yml.dist ├── examples ├── postman │ ├── tuneefy.postman_environment.json │ └── tuneefy_API.postman_collection.json ├── search.curl.php ├── search.curl.sh ├── search.request.js └── search.requests.py ├── gulpfile.js ├── migrations ├── .gitignore ├── Version20240101000000.php └── Version20240401205417.php ├── package.json ├── phpunit.xml.dist ├── public ├── build │ ├── css │ │ ├── embed.css │ │ ├── styles.css │ │ ├── widget-overlay.css │ │ └── widget.css │ └── js │ │ ├── about.js │ │ ├── main.js │ │ ├── search.js │ │ ├── show.js │ │ ├── trends.js │ │ ├── twig │ │ └── result.html.twig │ │ ├── vendor │ │ ├── iphone-style.js │ │ ├── jquery-3.6.0.min.js │ │ ├── raphael.min.js │ │ ├── tablesort.js │ │ └── twig.min.js │ │ └── widget.js ├── favicon.ico ├── favicon.png ├── img │ ├── ajax-loader-widget.gif │ ├── ajax-loader.gif │ ├── album_link.png │ ├── album_off.png │ ├── album_on.png │ ├── artist_link.png │ ├── background.png │ ├── background_menu.png │ ├── browsers.png │ ├── close.png │ ├── coverlay.png │ ├── creator_design.png │ ├── creator_idea.png │ ├── dashboard │ │ ├── album.png │ │ ├── song-alt.png │ │ ├── song.png │ │ └── users.png │ ├── error.png │ ├── extra_acoustic.png │ ├── extra_cover.png │ ├── extra_remix.png │ ├── fact_free.png │ ├── fact_friends.png │ ├── fact_patterns.png │ ├── fact_pertinence.png │ ├── fact_picks.png │ ├── fact_sharing.png │ ├── fact_widget.png │ ├── home_pic.png │ ├── icon_search.png │ ├── icon_search_help.png │ ├── iphone-style │ │ ├── off.png │ │ ├── on.png │ │ ├── slider.png │ │ ├── slider_center.png │ │ ├── slider_left.png │ │ └── slider_right.png │ ├── lang_underlay.png │ ├── logo.png │ ├── logo_footer.png │ ├── more_options.png │ ├── new_window.png │ ├── nothumb_album.png │ ├── nothumb_track.png │ ├── options_down.png │ ├── pagination_left.png │ ├── pagination_right.png │ ├── platforms │ │ ├── platform_amazon.png │ │ ├── platform_chart_amazon.png │ │ ├── platform_chart_deezer.png │ │ ├── platform_chart_groove.png │ │ ├── platform_chart_hypem.png │ │ ├── platform_chart_itunes.png │ │ ├── platform_chart_lastfm.png │ │ ├── platform_chart_mixcloud.png │ │ ├── platform_chart_napster.png │ │ ├── platform_chart_qobuz.png │ │ ├── platform_chart_soundcloud.png │ │ ├── platform_chart_spotify.png │ │ ├── platform_chart_tidal.png │ │ ├── platform_chart_youtube.png │ │ ├── platform_deezer.png │ │ ├── platform_full_amazon.png │ │ ├── platform_full_deezer.png │ │ ├── platform_full_groove.png │ │ ├── platform_full_hypem.png │ │ ├── platform_full_itunes.png │ │ ├── platform_full_lastfm.png │ │ ├── platform_full_mixcloud.png │ │ ├── platform_full_napster.png │ │ ├── platform_full_qobuz.png │ │ ├── platform_full_soundcloud.png │ │ ├── platform_full_spotify.png │ │ ├── platform_full_tidal.png │ │ ├── platform_full_youtube.png │ │ ├── platform_groove.png │ │ ├── platform_hypem.png │ │ ├── platform_itunes.png │ │ ├── platform_lastfm.png │ │ ├── platform_mixcloud.png │ │ ├── platform_napster.png │ │ ├── platform_qobuz.png │ │ ├── platform_soundcloud.png │ │ ├── platform_spotify.png │ │ ├── platform_tidal.png │ │ ├── platform_youtube.png │ │ └── platform_youtube_compliance.png │ ├── plus_options.png │ ├── reset.png │ ├── share.png │ ├── social.png │ ├── song_link.png │ ├── success.png │ ├── track_off.png │ ├── track_on.png │ ├── twitter.png │ └── widget_button.png └── index.php ├── src ├── Command │ ├── ExpiredIntentsCleanerCommand.php │ ├── PlatformIpsFetcherCommand.php │ └── StatsUpdaterCommand.php ├── Controller │ ├── ApiController.php │ ├── BackendController.php │ ├── FrontendController.php │ └── SecurityController.php ├── Dataclass │ └── MusicalEntity │ │ ├── Entities │ │ ├── Album.php │ │ └── Track.php │ │ ├── MusicalEntity.php │ │ ├── MusicalEntityInterface.php │ │ └── MusicalEntityMergeException.php ├── Entity │ ├── ApiClient.php │ └── Item.php ├── EventSubscriber │ ├── ApiBypassSubscriber.php │ ├── ApiStatsSubscriber.php │ └── LocaleSubscriber.php ├── Kernel.php ├── Repository │ ├── ApiClientRepository.php │ └── ItemRepository.php ├── Security │ ├── AdminUser.php │ ├── AdminUserProvider.php │ └── LoginFormAuthenticator.php ├── Serializer │ └── ApiErrorSerializer.php ├── Services │ ├── ClientCredentialsGrant.php │ ├── PlatformEngine.php │ ├── Platforms │ │ ├── DeezerPlatform.php │ │ ├── Interfaces │ │ │ ├── GeneralPlatformInterface.php │ │ │ ├── ScrobblingPlatformInterface.php │ │ │ ├── WebStoreInterface.php │ │ │ └── WebStreamingPlatformInterface.php │ │ ├── ItunesPlatform.php │ │ ├── LastFMPlatform.php │ │ ├── MixcloudPlatform.php │ │ ├── NapsterPlatform.php │ │ ├── Platform.php │ │ ├── PlatformException.php │ │ ├── PlatformResult.php │ │ ├── QobuzPlatform.php │ │ ├── SoundcloudPlatform.php │ │ ├── SpotifyPlatform.php │ │ ├── TidalPlatform.php │ │ └── YoutubePlatform.php │ └── StatsService.php └── Utils │ ├── ApiUtils.php │ └── Utils.php ├── symfony.lock ├── templates ├── _widget.html.twig ├── about.html.twig ├── admin │ ├── _base.html.twig │ ├── clients.html.twig │ ├── clients.new.html.twig │ ├── dashboard.html.twig │ └── login.html.twig ├── api.html ├── api │ ├── api.less │ └── main.apib ├── base.html.twig ├── bundles │ └── TwigBundle │ │ └── Exception │ │ ├── error.html.twig │ │ ├── error404.html.twig │ │ └── error503.html.twig ├── home.html.twig ├── item.album.html.twig ├── item.track.html.twig ├── macros │ └── tools.twig └── trends.html.twig ├── tests ├── Acceptance.suite.yml ├── Acceptance │ ├── BackendCest.php │ └── FrontendCest.php ├── Api.suite.yml ├── Api │ └── ApiCest.php ├── Support │ ├── AcceptanceTester.php │ ├── ApiTester.php │ ├── UnitTester.php │ └── _generated │ │ ├── AcceptanceTesterActions.php │ │ ├── ApiTesterActions.php │ │ └── UnitTesterActions.php ├── Unit.suite.yml ├── Unit │ ├── MusicalEntityTest.php │ ├── UtilsTest.php │ └── YoutubeVideoTitleParsingTest.php ├── _output │ └── .gitignore └── bootstrap.php ├── translations ├── messages.en.yaml └── messages.fr.yaml └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_ENV=dev 3 | APP_SECRET=4e544d8e15196020ef6cbf5526bc42d0 4 | ###< symfony/framework-bundle ### 5 | 6 | API_HOST=data.tuneefy.com 7 | WEBSITE_HOST=tuneefy.com 8 | 9 | ###> doctrine/doctrine-bundle ### 10 | DATABASE_URL="mysql://tuneefy:tuneefy@127.0.0.1:3306/tuneefy?serverVersion=10.11.2-MariaDB&charset=utf8mb4" 11 | ###< doctrine/doctrine-bundle ### 12 | 13 | ###> symfony/mailer ### 14 | MAILER_DSN="smtp://user:pass@smtp.domain.com:25" 15 | ###< symfony/mailer ### 16 | 17 | # Login for the administration dashboard 18 | ADMIN_LOGIN=admin 19 | ADMIN_PASSWORD=test 20 | 21 | # Google analytics 22 | GA_TRACKER_ID="00" 23 | 24 | # Captcha (mail sending from the about page) 25 | CAPTCHA_KEY="6L....." 26 | CAPTCHA_SECRET="6Ld......" 27 | 28 | # The identifier of the client to use to generate tokens when 29 | # bypassing the OAuth (for the tuneefy website) 30 | API_BYPASS_CLIENT_IDENTIFIER="create_an_oauth2_client_and_fetch_the_identifier" 31 | 32 | # Secret used to sign the intents (searches before they get persisted) 33 | INTENTS_SECRET="change_me_too" 34 | 35 | # Platforms 36 | DEEZER_KEY="" 37 | DEEZER_SECRET="" 38 | 39 | SPOTIFY_KEY="" 40 | SPOTIFY_SECRET="" 41 | 42 | QOBUZ_KEY="" 43 | QOBUZ_SECRET="" 44 | 45 | LASTFM_KEY="" 46 | LASTFM_SECRET="" 47 | 48 | SOUNDCLOUD_KEY="" 49 | SOUNDCLOUD_SECRET="" 50 | 51 | YOUTUBE_KEY="" 52 | YOUTUBE_SECRET="" 53 | 54 | MIXCLOUD_KEY="" 55 | MIXCLOUD_SECRET="" 56 | 57 | ITUNES_KEY="" 58 | ITUNES_SECRET="" 59 | 60 | TIDAL_KEY="" 61 | TIDAL_SECRET="" 62 | 63 | NAPSTER_KEY="" 64 | NAPSTER_SECRET="" 65 | 66 | ###> league/oauth2-server-bundle ### 67 | OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.pem 68 | OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem 69 | OAUTH_PASSPHRASE= 70 | OAUTH_ENCRYPTION_KEY=changeMe 71 | ###< league/oauth2-server-bundle ### 72 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | 8 | LOG_ENABLED=0 9 | 10 | TEST_API_CLIENT_ID=test_id 11 | TEST_API_CLIENT_SECRET=test_secret -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | /.env.local 3 | /.env.local.php 4 | /.env.*.local 5 | /config/secrets/prod/prod.decrypt.private.php 6 | /public/bundles/ 7 | /var/ 8 | /vendor/ 9 | ###< symfony/framework-bundle ### 10 | 11 | ###> phpunit/phpunit ### 12 | /phpunit.xml 13 | .phpunit.result.cache 14 | ###< phpunit/phpunit ### 15 | 16 | ###> symfony/phpunit-bridge ### 17 | .phpunit.result.cache 18 | /phpunit.xml 19 | ###< symfony/phpunit-bridge ### 20 | 21 | ###> symfony/asset-mapper ### 22 | /public/assets/ 23 | /assets/vendor/ 24 | ###< symfony/asset-mapper ### 25 | 26 | # Deploy files 27 | deploy/vars.yml 28 | deploy/*.prod.yml 29 | 30 | # Generic files 31 | .DS_Store 32 | TODO.* 33 | 34 | ## Frontend 35 | node_modules 36 | /web/build 37 | 38 | ###> friendsofphp/php-cs-fixer ### 39 | /.php-cs-fixer.php 40 | /.php-cs-fixer.cache 41 | ###< friendsofphp/php-cs-fixer ### 42 | 43 | ###> league/oauth2-server-bundle ### 44 | /config/jwt/*.pem 45 | ###< league/oauth2-server-bundle ### 46 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__.'/src') 5 | ->in(__DIR__.'/tests') 6 | ->exclude(['_support', '_data', '_output']) 7 | ; 8 | 9 | return (new PhpCsFixer\Config()) 10 | ->setRules(array( 11 | '@Symfony' => true, 12 | 'ordered_imports' => true, // Order "use" alphabetically 13 | 'array_syntax' => ['syntax' => 'short'], // Replace array() by [] 14 | 'no_useless_return' => true, // Keep return null; 15 | 'phpdoc_order' => true, // Clean up the /** php doc */ 16 | 'linebreak_after_opening_tag' => true, 17 | 'multiline_whitespace_before_semicolons' => false, 18 | 'phpdoc_add_missing_param_annotation' => true, 19 | 'phpdoc_order' => true, 20 | )) 21 | ->setUsingCache(false) 22 | ->setFinder($finder) 23 | ; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tuneefy _2_ 2 | 3 | A new version of [tuneefy](http://tuneefy.com) built for **PHP 8** and **Node 18+**, from the ground up, using Symfony and a few helper libraries. 4 | 5 | ### Installing 6 | 7 | This project uses [composer 2](https://getcomposer.org/). Just run : 8 | 9 | composer install 10 | 11 | ### Creating tables 12 | 13 | Tuneefy needs a variety of tables to work properly; you can populate your database with the following : 14 | 15 | bin/console doctrine:migrations:migrate 16 | 17 | ### Building assets & API doc 18 | 19 | To build the assets and the API documentation, I use **yarn** and some modules. 20 | 21 | yarn install 22 | 23 | yarn run build 24 | yarn run api-documentation 25 | 26 | ### Tests 27 | 28 | The tests are under the `./tests` folder and I use **Codeception** to run them. 29 | 30 | Just run : 31 | 32 | vendor/bin/codecept run --steps 33 | 34 | Beforehand, do not forget to launch a development web server so that the functional tests have an endpoint to test: 35 | 36 | symfony server:start --port 9999 37 | 38 | There should be 40 tests containing 697 assertions. 39 | 40 | > Sometimes a platform fails to respond correctly due to network latencies or such. Re-run the tests in this case, it should pass fine the second time. 41 | 42 | ### API 43 | 44 | The API endpoints require an OAuth access token. The token is necessary to authenticate **all** requests to the API. 45 | 46 | The tuneefy API currently supports the [OAuth 2 draft](https://oauth.net/2/) specification. All OAuth2 requests MUST use the SSL endpoint available at https://data.tuneefy.com/. 47 | 48 | OAuth 2.0 is a simple and secure authentication mechanism. It allows applications to acquire an access token for tuneefy via a POST request to a token endpoint. Authentication with OAuth can be accomplished in the following steps: 49 | 50 | 1. Register for an API key by sending a mail to api@tuneefy.com 51 | 2. Exchange your customer id and secret for an access token 52 | 3. Make requests by passing the token in the Authorization header 53 | 4. When your token expires, you can get a new one 54 | 55 | #### Apply for an API key 56 | 57 | You can get an API key and associated secret by sending an email to api@tuneefy.com. 58 | 59 | #### Web Service Rate Limits 60 | 61 | Limits are placed on the number of API requests you may make using your API key. Rate limits may vary by service, but the defaults are 100 requests per hour. 62 | 63 | #### Full documentation 64 | 65 | The full documentation is available at https://data.tuneefy.com. An API blueprint is also available [here](https://github.com/tchapi/tuneefy2/blob/master/app/templates/api/main.apib) — use your preferred renderer to build it. We use Aglio. 66 | 67 | 68 | - - - 69 | 70 | > If you want to participate/contribute, feel free to create pull requests or issues so we can make Tuneefy better and more efficient ! 71 | -------------------------------------------------------------------------------- /assets/js/about.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var u = $('ul.platformsPatterns li.platform') 3 | 4 | $('form.contactForm #send').click(function () { 5 | var e = $('form.contactForm #email') 6 | var m = $('form.contactForm #message') 7 | var c = $('form.contactForm #g-recaptcha-response') 8 | var action = $('form').attr('action') 9 | 10 | m.removeClass('error') 11 | e.removeClass('error') 12 | 13 | if (e.val() == '') { 14 | e.addClass('error') 15 | return false 16 | } 17 | 18 | if (m.val() == '') { 19 | m.addClass('error') 20 | return false 21 | } 22 | 23 | $('form.contactForm').hide() 24 | $('.waitingMail').show() 25 | 26 | $.post(action, { mail: e.val(), message: m.val(), captcha: c.val() }, function (data) { 27 | $('.waitingMail').hide() 28 | 29 | if (data == '1') { 30 | $('.successMail').show() 31 | } else { 32 | $('.errorMail').show() 33 | } 34 | }) 35 | }) 36 | 37 | u.click(function (e) { 38 | if (!$(e.target).hasClass('platform')) return 39 | u.removeClass('active') 40 | $(e.target).addClass('active') 41 | u.find('ul').hide() 42 | $(e.target).find('ul').show() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('#lang span').click(function (e) { 3 | var value = 'tuneefyLocale=' + $(e.target).attr('lang') + '; ' 4 | value += 'expires=Sat, 01 Feb 2042 01:20:42 GMT; path=/; domain= ' + $DOMAIN + ';' 5 | document.cookie = value 6 | location.reload() 7 | }) 8 | 9 | $(document).on('click', '.backToTop', function (e) { 10 | e.preventDefault() 11 | $('html,body').animate({ 12 | scrollTop: 0 13 | }, 1500) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /assets/js/show.js: -------------------------------------------------------------------------------- 1 | function toggleEmbed() { 2 | $('#embedHolder').toggle() 3 | $('#embed').toggleClass('open') 4 | }; 5 | 6 | function newTweet(status) { 7 | var width = 575; var height = 400 8 | 9 | var left = ($(window).width() - width) / 2 10 | 11 | var top = ($(window).height() - height) / 2 12 | 13 | var url = 'https://twitter.com/home?status=' + status 14 | 15 | var opts = 'status=1' + 16 | ',width=' + width + 17 | ',height=' + height + 18 | ',top=' + top + 19 | ',left=' + left 20 | 21 | window.open(url, 'twitter', opts) 22 | }; 23 | 24 | $(document).ready(function () { 25 | $('#mainLink, #embedContent').click(function (e) { 26 | $(e.target).focus() 27 | $(e.target).select() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /assets/js/trends.js: -------------------------------------------------------------------------------- 1 | Raphael.fn.pieChart = function (cx, cy, r, values, labels, stroke) { 2 | // We create the paper 3 | var paper = this 4 | 5 | var rad = Math.PI / 180 6 | 7 | var chart = this.set() 8 | 9 | // Function to create a sector 10 | function sector(cx, cy, r, startAngle, endAngle, params) { 11 | var x1 = cx + r * Math.cos(-startAngle * rad) 12 | 13 | var x2 = cx + r * Math.cos(-endAngle * rad) 14 | 15 | var y1 = cy + r * Math.sin(-startAngle * rad) 16 | 17 | var y2 = cy + r * Math.sin(-endAngle * rad) 18 | return paper.path(['M', cx, cy, 'L', x1, y1, 'A', r, r, 0, +(endAngle - startAngle > 180), 0, x2, y2, 'z']).attr(params) 19 | }; 20 | 21 | var angle = 0 22 | 23 | var total = 0 24 | 25 | var start = 0 26 | 27 | var process = function (j) { 28 | var value = values[j] 29 | 30 | var angleplus = 360 * value / total 31 | 32 | var popangle = angle + (angleplus / 2) 33 | 34 | var ms = 500 35 | 36 | var delta = 20 37 | 38 | var p = sector(cx, cy, r, angle, angle + angleplus, { fill: Raphael.color('#' + labels[j][2]), stroke: 'none' }) 39 | 40 | var txt = paper.text(cx + (r + delta + 55) * Math.cos(-popangle * rad), cy + (r + delta + 25) * Math.sin(-popangle * rad), labels[j][1]).attr({ fill: Raphael.color('#' + labels[j][2]), stroke: 'none', opacity: 0, 'font-size': 16 }) 41 | if (angleplus > 13) { paper.image('/img/platforms/platform_chart_' + labels[j][0] + '.png', cx + (r * 0.65) * Math.cos(-popangle * rad) - 18, cy + (r * 0.65) * Math.sin(-popangle * rad) - 15, 36, 30) } 42 | p.mouseover(function () { 43 | p.stop().animate({ transform: 's1.1 1.1 ' + cx + ' ' + cy }, ms, 'elastic') 44 | txt.stop().animate({ opacity: 1 }, ms, 'elastic') 45 | }).mouseout(function () { 46 | p.stop().animate({ transform: '' }, ms, 'elastic') 47 | txt.stop().animate({ opacity: 0 }, ms) 48 | }) 49 | angle += angleplus 50 | chart.push(p) 51 | chart.push(txt) 52 | start += 0.1 53 | } 54 | 55 | // For each value, we create the corresponding sector 56 | for (var i = 0, ii = values.length; i < ii; i++) { 57 | total += values[i] 58 | } 59 | for (i = 0; i < ii; i++) { 60 | process(i) 61 | } 62 | 63 | // Underlying grey circle 64 | paper.circle(cx, cy, r + 10).attr({ fill: '#2d2d2d', stroke: 'none' }).toBack() 65 | 66 | return chart 67 | } 68 | 69 | $(document).ready(function () { 70 | var values = []; var labels = [] 71 | $('table#pieData tr').each(function () { 72 | values.push(parseInt($('td', this).text(), 10)) 73 | labels.push(new Array($('th span.id', this).text(), $('th span.name', this).text(), $('th span.color', this).text())) 74 | }) 75 | var w = $('#pieChart').outerWidth() 76 | var h = $('#pieChart').outerHeight() 77 | Raphael('pieChart', w, 500).pieChart(w / 2, 250, Math.min(h / 2.5, w / 2.5), values, labels, '#2d2d2d') 78 | }) 79 | -------------------------------------------------------------------------------- /assets/js/twig/result.html.twig: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | {%- if type == "track" -%} 4 | 5 | {%- else -%} 6 | 7 | {%- endif -%} 8 |
    9 |
    10 | {% if type == "track" %} 11 |
    12 |
    {{ item.safe_title }} 13 |
    14 | {%- if item.extra_info.is_cover or item.album.extra_info.is_cover -%} 15 | [Cover]  16 | {%- endif -%} 17 | {%- if item.extra_info.is_remix -%} 18 | [Remix]  19 | {%- endif -%} 20 | {%- if item.extra_info.acoustic -%} 21 | [Acoustic]  22 | {%- endif -%} 23 |
    24 |
    25 |
    26 |
    27 |
    {{ item.album.artist }}
    28 |
    29 |
    30 |
    {{ item.album.safe_title }}
    31 |
    32 | {%- else -%} 33 |
    34 |
    {{ item.safe_title }} 35 |
    36 | {%- if item.extra_info.is_cover -%} 37 | [Cover]  38 | {%- endif -%} 39 | {%- if item.extra_info.is_remix -%} 40 | [Remix]  41 | {%- endif -%} 42 | {%- if item.extra_info.acoustic -%} 43 | [Acoustic]  44 | {%- endif -%} 45 |
    46 |
    47 |
    48 |
    49 |
    {{ item.artist }}
    50 |
    51 | {%- endif -%} 52 | 65 | {{ share }} 66 |
  • -------------------------------------------------------------------------------- /assets/js/vendor/tablesort.js: -------------------------------------------------------------------------------- 1 | /* 2 | A simple, lightweight jQuery plugin for creating sortable tables. 3 | https://github.com/kylefox/jquery-tablesort 4 | Version 0.0.11 5 | */ 6 | 7 | (function($) { 8 | $.tablesort = function ($table, settings) { 9 | var self = this; 10 | this.$table = $table; 11 | this.$thead = this.$table.find('thead'); 12 | this.settings = $.extend({}, $.tablesort.defaults, settings); 13 | this.$sortCells = this.$thead.length > 0 ? this.$thead.find('th:not(.no-sort)') : this.$table.find('th:not(.no-sort)'); 14 | this.$sortCells.on('click.tablesort', function() { 15 | self.sort($(this)); 16 | }); 17 | this.index = null; 18 | this.$th = null; 19 | this.direction = null; 20 | }; 21 | 22 | $.tablesort.prototype = { 23 | 24 | sort: function(th, direction) { 25 | var start = new Date(), 26 | self = this, 27 | table = this.$table, 28 | rowsContainer = table.find('tbody').length > 0 ? table.find('tbody') : table, 29 | rows = rowsContainer.find('tr').has('td, th'), 30 | cells = rows.find(':nth-child(' + (th.index() + 1) + ')').filter('td, th'), 31 | sortBy = th.data().sortBy, 32 | sortedMap = []; 33 | 34 | var unsortedValues = cells.map(function(idx, cell) { 35 | if (sortBy) 36 | return (typeof sortBy === 'function') ? sortBy($(th), $(cell), self) : sortBy; 37 | return ($(this).data().sortValue != null ? $(this).data().sortValue : $(this).text()); 38 | }); 39 | if (unsortedValues.length === 0) return; 40 | 41 | //click on a different column 42 | if (this.index !== th.index()) { 43 | this.direction = 'asc'; 44 | this.index = th.index(); 45 | } 46 | else if (direction !== 'asc' && direction !== 'desc') 47 | this.direction = this.direction === 'asc' ? 'desc' : 'asc'; 48 | else 49 | this.direction = direction; 50 | 51 | direction = this.direction == 'asc' ? 1 : -1; 52 | 53 | self.$table.trigger('tablesort:start', [self]); 54 | self.log("Sorting by " + this.index + ' ' + this.direction); 55 | 56 | // Try to force a browser redraw 57 | self.$table.css("display"); 58 | // Run sorting asynchronously on a timeout to force browser redraw after 59 | // `tablesort:start` callback. Also avoids locking up the browser too much. 60 | setTimeout(function() { 61 | self.$sortCells.removeClass(self.settings.asc + ' ' + self.settings.desc); 62 | for (var i = 0, length = unsortedValues.length; i < length; i++) 63 | { 64 | sortedMap.push({ 65 | index: i, 66 | cell: cells[i], 67 | row: rows[i], 68 | value: unsortedValues[i] 69 | }); 70 | } 71 | 72 | sortedMap.sort(function(a, b) { 73 | return self.settings.compare(a.value, b.value) * direction; 74 | }); 75 | 76 | $.each(sortedMap, function(i, entry) { 77 | rowsContainer.append(entry.row); 78 | }); 79 | 80 | th.addClass(self.settings[self.direction]); 81 | 82 | self.log('Sort finished in ' + ((new Date()).getTime() - start.getTime()) + 'ms'); 83 | self.$table.trigger('tablesort:complete', [self]); 84 | //Try to force a browser redraw 85 | self.$table.css("display"); 86 | }, unsortedValues.length > 2000 ? 200 : 10); 87 | }, 88 | 89 | log: function(msg) { 90 | if(($.tablesort.DEBUG || this.settings.debug) && console && console.log) { 91 | console.log('[tablesort] ' + msg); 92 | } 93 | }, 94 | 95 | destroy: function() { 96 | this.$sortCells.off('click.tablesort'); 97 | this.$table.data('tablesort', null); 98 | return null; 99 | } 100 | 101 | }; 102 | 103 | $.tablesort.DEBUG = false; 104 | 105 | $.tablesort.defaults = { 106 | debug: $.tablesort.DEBUG, 107 | asc: 'sorted ascending', 108 | desc: 'sorted descending', 109 | compare: function(a, b) { 110 | if (a > b) { 111 | return 1; 112 | } else if (a < b) { 113 | return -1; 114 | } else { 115 | return 0; 116 | } 117 | } 118 | }; 119 | 120 | $.fn.tablesort = function(settings) { 121 | var table, sortable, previous; 122 | return this.each(function() { 123 | table = $(this); 124 | previous = table.data('tablesort'); 125 | if(previous) { 126 | previous.destroy(); 127 | } 128 | table.data('tablesort', new $.tablesort(table, settings)); 129 | }); 130 | }; 131 | 132 | })(window.Zepto || window.jQuery); -------------------------------------------------------------------------------- /assets/scss/partials/embed.scss: -------------------------------------------------------------------------------- 1 | /* OVER RIDE */ 2 | body {font-size: 0.7em;} 3 | #footer-wrapper, #header-wrapper {display: none;} 4 | .wrap {width: auto; margin: 0;} 5 | .wrap.bdBot, #trackInfo.bdBot{border: none;} 6 | .Bcolor{background: transparent;} 7 | 8 | #wrapper { 9 | background: #151515; 10 | border-radius: 3px; 11 | position: relative; 12 | box-shadow: 0 1px #070707 inset, 0 1px #333333; 13 | } 14 | 15 | /* TAG LINE */ 16 | #tagline{ 17 | height: 36px; 18 | padding: 0; 19 | } 20 | 21 | h1.logo{ 22 | display: inline-block; 23 | margin-left: -6px; 24 | margin-top: -6px; 25 | & > img { 26 | width: 100px; 27 | height: auto 28 | } 29 | } 30 | 31 | p.tagline{ 32 | float: right; 33 | padding-top: 12px; 34 | padding-right: 10px; 35 | font-size: 1em; 36 | color: #696969; 37 | } 38 | 39 | #content { 40 | padding: 0; 41 | } 42 | 43 | /* SHARE */ 44 | #mainTitle{ 45 | font-size: 1.7em; 46 | text-align: center; 47 | padding-top: 50px; 48 | } 49 | 50 | #mainContent{ 51 | width: 100%; 52 | margin: 0; 53 | } 54 | 55 | #trackInfo{ 56 | height: 90px; 57 | padding: 8px; 58 | } 59 | 60 | #trackInfo .cover{ 61 | float: left; 62 | position: relative; 63 | margin: 0 8px 0 0; 64 | } 65 | 66 | #trackInfo .coverlay{ 67 | width: 80px; 68 | height: 80px; 69 | } 70 | 71 | #trackInfo .info{ 72 | margin: 0; 73 | } 74 | 75 | #trackInfo .infoWrapper{ 76 | 77 | } 78 | 79 | #trackInfo .infoContent{ 80 | vertical-align: middle; 81 | } 82 | 83 | #trackInfo .title{ 84 | font-weight: bold; 85 | font-size:1.5em; 86 | line-height: 0.9em; 87 | color: #A2A2A2; 88 | padding-top: 4px; 89 | } 90 | 91 | #trackInfo .artist{ 92 | font-size: 1.2em; 93 | line-height: 1.3em; 94 | color: #474747; 95 | padding-top: 0; 96 | } 97 | 98 | #trackInfo .album{ 99 | font-size: 0.75em; 100 | color: #474747; 101 | text-transform: uppercase; 102 | } 103 | 104 | #platforms, #share{ 105 | padding: 0px; 106 | } 107 | #platforms{ 108 | padding-top: 1px; 109 | } 110 | 111 | #platforms .listenTitle, #share .mainTitle{ 112 | text-transform: uppercase; 113 | font-size: 0.75em; 114 | color: #AAAAAA; 115 | margin-bottom: 5px; 116 | } 117 | 118 | #platforms .btns_full{ 119 | margin-right: 3px; 120 | margin-bottom: 0px; 121 | } 122 | 123 | #linkHolder, #externalPlatformsActions{ 124 | background: #1F1F1F; 125 | float: left; 126 | border: 1px solid black; 127 | padding: 8px; 128 | position: relative; 129 | border-radius: 3px; 130 | } 131 | #linkHolder{ 132 | margin-right: 10px; 133 | } 134 | 135 | .newWindow{ 136 | opacity: 0.9; 137 | margin-left: 3px; 138 | } 139 | 140 | @media (max-width: 500px) { 141 | h1.logo { 142 | width: auto; 143 | } 144 | #tagline { 145 | text-align: left; 146 | } 147 | #tagline p { 148 | float: right; 149 | padding-top: 12px; 150 | padding-right: 10px; 151 | } 152 | #trackInfo { 153 | height: auto; 154 | overflow: auto; 155 | } 156 | #trackInfo .info { 157 | width: auto; 158 | } 159 | } 160 | 161 | @media (max-width: 310px) { 162 | #tagline { 163 | height: auto; 164 | } 165 | #tagline p { 166 | display: block; 167 | padding: 0; 168 | float: none; 169 | margin: 0 0 8px 12px; 170 | } 171 | } -------------------------------------------------------------------------------- /assets/scss/partials/iphone-style.scss: -------------------------------------------------------------------------------- 1 | .iPhoneCheckContainer, 2 | .iPhoneCheckContainer2 { 3 | position: relative; 4 | height: 27px; 5 | cursor: pointer; 6 | overflow: hidden; } 7 | .iPhoneCheckContainer input, 8 | .iPhoneCheckContainer2 input { 9 | position: absolute; 10 | top: 5px; 11 | left: 30px; 12 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); 13 | opacity: 0; } 14 | .iPhoneCheckContainer label, 15 | .iPhoneCheckContainer2 label { 16 | white-space: nowrap; 17 | font-size: 17px; 18 | line-height: 17px; 19 | font-weight: bold; 20 | font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; 21 | cursor: pointer; 22 | display: block; 23 | height: 27px; 24 | position: absolute; 25 | width: auto; 26 | top: 0; 27 | padding-top: 5px; 28 | overflow: hidden; } 29 | .iPhoneCheckContainer, .iPhoneCheckContainer2, .iPhoneCheckContainer label, .iPhoneCheckContainer2 label { 30 | user-select: none; 31 | -moz-user-select: none; 32 | -khtml-user-select: none; } 33 | 34 | .iPhoneCheckDisabled { 35 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50); 36 | opacity: 0.5; } 37 | 38 | label.iPhoneCheckLabelOn, 39 | label.iPhoneCheckLabelOn2 40 | { 41 | color: white; 42 | background: url('../../img/iphone-style/on.png') no-repeat; 43 | text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.6); 44 | left: 0; 45 | padding-top: 5px; } 46 | label.iPhoneCheckLabelOn span { 47 | padding-left: 8px; } 48 | label.iPhoneCheckLabelOff, 49 | label.iPhoneCheckLabelOff2 { 50 | color: #8b8b8b; 51 | background: url('../../img/iphone-style/off.png') no-repeat right 0; 52 | text-shadow: 0px 0px 2px rgba(255, 255, 255, 0.6); 53 | text-align: right; 54 | right: 0; } 55 | label.iPhoneCheckLabelOff span { 56 | padding-right: 8px; } 57 | 58 | .iPhoneCheckHandle, 59 | .iPhoneCheckHandle2 { 60 | display: block; 61 | height: 27px; 62 | cursor: pointer; 63 | position: absolute; 64 | top: 0; 65 | left: 0; 66 | width: 0; 67 | background: url('../../img/iphone-style/slider_left.png') no-repeat; 68 | padding-left: 3px; } 69 | 70 | .iPhoneCheckHandle2{ 71 | background: blue; 72 | border-radius: 3px; 73 | } 74 | 75 | .iPhoneCheckHandleRight, 76 | .iPhoneCheckHandleRight2 { 77 | height: 100%; 78 | width: 100%; 79 | padding-right: 3px; 80 | background: url('../../img/iphone-style/slider_right.png') no-repeat right 0; } 81 | 82 | .iPhoneCheckHandleCenter, 83 | .iPhoneCheckHandleCenter2 { 84 | height: 100%; 85 | width: 100%; 86 | background: url('../../img/iphone-style/slider_center.png'); } 87 | 88 | /* tchap : for CSS */ 89 | .iPhoneCheckContainer { width: 92px; } 90 | .iPhoneCheckHandle { width: 40px; } 91 | 92 | /* Tracks and albums checkbox */ 93 | .noBG { 94 | background: none !important; 95 | } 96 | 97 | 98 | .otherContainer { 99 | border: 1px solid black; 100 | margin: 0 10px; 101 | background: #282828 !important; 102 | border-radius: 4px; 103 | box-shadow: 0 0 5px black inset, 0 1px #282828; 104 | } 105 | 106 | .otherHandle { 107 | width: 42px; 108 | top: -1px; 109 | border: 1px solid black; 110 | background: #222; 111 | background-image: -moz-linear-gradient(center top , #333333 0%, #181818 100%); 112 | background-image: -webkit-gradient(linear, left top,left bottom, color-stop(0,#333333), color-stop(1,#181818)); 113 | border-radius: 4px; 114 | margin-left: -1px; 115 | } 116 | 117 | .activeTracks{ 118 | background: url('../../img/track_on.png') no-repeat 9px 3px; 119 | } 120 | .activeAlbums{ 121 | background: url('../../img/album_on.png') no-repeat 9px 3px; 122 | } 123 | 124 | .otherContainer label { 125 | height: 22px; 126 | } 127 | 128 | label.iPhoneCheckLabelOn.albums{ 129 | background: url('../../img/album_off.png') no-repeat 13px 4px; 130 | } 131 | label.iPhoneCheckLabelOff.tracks{ 132 | background: url('../../img/track_off.png') no-repeat 54px 3px; 133 | } -------------------------------------------------------------------------------- /assets/scss/partials/reset.scss: -------------------------------------------------------------------------------- 1 | /* Based on http://meyerweb.com/eric/tools/css/reset/ */ 2 | html, body, div, span, applet, object, iframe, 3 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 4 | a, abbr, acronym, address, big, cite, code, 5 | del, dfn, em, img, ins, kbd, q, s, samp, 6 | small, strike, strong, sub, sup, tt, var, 7 | b, u, i, center, 8 | dl, dt, dd, ol, ul, li, 9 | fieldset, form, label, legend, 10 | table, caption, tbody, tfoot, thead, tr, th, td, 11 | article, aside, canvas, details, embed, 12 | figure, figcaption, footer, header, hgroup, 13 | menu, nav, output, ruby, section, summary, 14 | time, mark, audio, video { 15 | margin: 0; 16 | padding: 0; 17 | border: 0; 18 | font-size: 100%; 19 | font: inherit; 20 | vertical-align: baseline; 21 | } 22 | /* HTML5 display-role reset for older browsers */ 23 | article, aside, details, figcaption, figure, 24 | footer, header, hgroup, menu, nav, section { 25 | display: block; 26 | } 27 | body { 28 | line-height: 1; 29 | } 30 | ol, ul { 31 | list-style: none; 32 | } 33 | blockquote, q { 34 | quotes: none; 35 | } 36 | blockquote:before, blockquote:after, 37 | q:before, q:after { 38 | content: ''; 39 | content: none; 40 | } 41 | table { 42 | border-collapse: collapse; 43 | border-spacing: 0; 44 | } 45 | 46 | a, a:link { text-decoration: none; } 47 | 48 | input, textarea{ 49 | outline: none; /* for Safari by tchap */ 50 | } -------------------------------------------------------------------------------- /assets/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'partials/reset'; 2 | @import 'partials/general'; 3 | @import 'partials/style'; 4 | @import 'partials/iphone-style'; 5 | 6 | @import 'partials/media-queries'; 7 | -------------------------------------------------------------------------------- /assets/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: skyblue; 3 | } 4 | -------------------------------------------------------------------------------- /assets/widget/widget-overlay.scss: -------------------------------------------------------------------------------- 1 | div#tuneefy_overlay { 2 | position: fixed; 3 | display: block; 4 | width: 450px; 5 | height: 180px; 6 | /*top: -180px;*/ 7 | top:0; 8 | right: -2px; 9 | background: #1c1c1c; 10 | border: 1px solid black; 11 | border-radius: 0 0 0 4px; 12 | box-shadow: 0 0 6px black; 13 | z-index: 1000 !important; 14 | } 15 | 16 | #tuneefy_overlay div.closeButton { 17 | position: absolute; 18 | display: block; 19 | top: 10px; 20 | right: 11px; 21 | background: url("//tuneefy.com/img/close.png") no-repeat scroll center top transparent; 22 | height: 32px; 23 | width: 32px; 24 | cursor: pointer; 25 | } 26 | 27 | #tuneefy_overlay div.closeButton:hover{ 28 | background-position: 0px -32px; 29 | } 30 | 31 | #tuneefy_overlay iframe{ 32 | border: none; 33 | background: transparent; 34 | height: 100%; 35 | width: 100%; 36 | } 37 | 38 | #tuneefy_overlay .middleBox{ 39 | position: relative; 40 | top: 66px; 41 | left: 200px; 42 | } 43 | -------------------------------------------------------------------------------- /assets/widget/widget.scss: -------------------------------------------------------------------------------- 1 | body{ 2 | font: normal normal normal 1em/1em arial,helvetica,sans-serif; 3 | } 4 | 5 | /* Button CSS 3 */ 6 | a.btn{ 7 | position: relative; 8 | color: white; 9 | text-transform: lowercase; 10 | font-size: 14px; 11 | font-weight: bold; 12 | display: block; 13 | background: #222; 14 | background-image: -moz-linear-gradient(center top , #262626 0%, #1B1B1B 100%); 15 | background-image: -webkit-gradient(linear, left top,left bottom, color-stop(0,#262626), color-stop(1,#1B1B1B)); 16 | border: 1px solid black; 17 | border-radius: 3px; 18 | padding: 6px 15px 7px 15px; 19 | cursor: pointer; 20 | text-shadow: 0 -2px black; 21 | box-shadow: 0 1px #333333 inset, 0 1px #171717; 22 | } 23 | a.btn:hover{ color: #76bad0; } 24 | a.btn:active{ box-shadow: 0 3px 3px #171717 inset, 0 1px #171717; } 25 | 26 | a.btns{ 27 | display: inline-block; 28 | width: 36px; 29 | height: 31px; 30 | cursor: pointer; 31 | } 32 | 33 | a.btns:hover{ background-position: 0px -32px; } 34 | a.btns.off{ background-position: 0px -64px; } 35 | 36 | a.btn_deezer{ background: url("../img/platforms/platform_deezer.png") no-repeat;} 37 | a.btn_spotify{ background: url("../img/platforms/platform_spotify.png") no-repeat;} 38 | a.btn_qobuz{ background: url("../img/platforms/platform_qobuz.png") no-repeat;} 39 | a.btn_lastfm{ background: url("../img/platforms/platform_lastfm.png") no-repeat;} 40 | a.btn_itunes{ background: url("../img/platforms/platform_itunes.png") no-repeat;} 41 | a.btn_youtube{ background: url("../img/platforms/platform_youtube.png") no-repeat;} 42 | a.btn_soundcloud{ background: url("../img/platforms/platform_soundcloud.png") no-repeat;} 43 | a.btn_mixcloud{ background: url("../img/platforms/platform_mixcloud.png") no-repeat;} 44 | a.btn_groove{ background: url("../img/platforms/platform_groove.png") no-repeat;} 45 | a.btn_napster{ background: url("../img/platforms/platform_napster.png") no-repeat;} 46 | a.btn_tidal{ background: url("../img/platforms/platform_tidal.png") no-repeat;} 47 | a.btn_amazon{ background: url("../img/platforms/platform_amazon.png") no-repeat;} 48 | a.btn_hypem{ background: url("../img/platforms/platform_hypem.png") no-repeat;} 49 | 50 | .txtS{ text-shadow: 0 -1px black !important; } /*standard*/ 51 | 52 | ul li.tHeader{ 53 | display: none; 54 | } 55 | 56 | #waiting{ 57 | background: #1A1A1A; 58 | height: 100%; 59 | left: 0; 60 | opacity: 0.8; 61 | position: absolute; 62 | top: 0; 63 | width: 100%; 64 | z-index: 10; 65 | } 66 | 67 | #waiting img{ 68 | position: absolute; 69 | left: 200px; 70 | top: 66px; 71 | } 72 | 73 | #results{ 74 | padding: 16px; 75 | } 76 | 77 | .nbResults{ 78 | color: #76bad0; 79 | font-size: 18px; 80 | text-shadow: 0 -1px black; 81 | margin-bottom: 10px; 82 | } 83 | 84 | li.tResult{ 85 | box-shadow: 0 1px #070707 inset, 0 1px #333333; 86 | border-radius: 3px; 87 | background: #151515; 88 | height: 60px; 89 | padding: 10px; 90 | } 91 | 92 | li.tResult .coverlay{ 93 | position:absolute; 94 | top:4px; 95 | left:4px; 96 | width: 48px; 97 | height: 48px; 98 | border: 1px solid #AAA; 99 | background: url("../img/coverlay.png") center center; 100 | } 101 | 102 | li.tResult .tImage{ 103 | float: left; 104 | position: relative; 105 | margin-right: 20px; 106 | } 107 | 108 | li.tResult .tImage img{ 109 | width: 50px; 110 | height: 50px; 111 | border: 4px solid white; 112 | box-shadow: 0px 0px 3px black; 113 | display: block; 114 | } 115 | 116 | li.tResult .tTitle{ 117 | font-weight: bold; 118 | font-size: 16px; 119 | color: #A2A2A2; 120 | padding-top: 4px; 121 | } 122 | 123 | li.tResult .tFeat{ 124 | display: none; 125 | } 126 | 127 | li.tResult .tArtist{ 128 | color: #474747; 129 | font-size: 12px; 130 | padding-top: 4px; 131 | } 132 | 133 | li.tResult .tAlbum{ 134 | color: #474747; 135 | font-size: 10px; 136 | padding-top: 2px; 137 | text-transform: uppercase; 138 | } 139 | 140 | li.tResult .tShare{ 141 | position: absolute; 142 | bottom: 14px; 143 | right: 138px; 144 | } 145 | 146 | .tLinks{ 147 | position: absolute; 148 | bottom: 11px; 149 | left: 14px; 150 | } 151 | 152 | .tLinks .wrapper{ 153 | height: 33px; 154 | overflow: hidden; 155 | width: 205px; 156 | } 157 | 158 | #more { 159 | position: absolute; 160 | bottom: 14px; 161 | right: 16px; 162 | } 163 | 164 | #alerts{ 165 | height: 100%; 166 | width: 100%; 167 | text-align: center; 168 | padding-top: 66px; 169 | } 170 | #alerts span{ 171 | text-shadow: 0 -1px black !important; 172 | color: #76bad0; 173 | } 174 | 175 | a.backToTop{ 176 | display: none !important; 177 | } -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = 80000) { 10 | require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; 11 | } else { 12 | define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); 13 | require PHPUNIT_COMPOSER_INSTALL; 14 | PHPUnit\TextUI\Command::main(); 15 | } 16 | } else { 17 | if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { 18 | echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; 19 | exit(1); 20 | } 21 | 22 | require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; 23 | } 24 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | # Suite configuration 2 | # Assumes your local dev server is at 127.0.0.1:9999: 3 | # 4 | # symfony server:start --port 9999 5 | # 6 | 7 | namespace: Tests 8 | support_namespace: Support 9 | paths: 10 | tests: tests 11 | output: tests/_output 12 | data: tests/Support/Data 13 | support: tests/Support 14 | envs: tests/_envs 15 | actor_suffix: Tester 16 | extensions: 17 | enabled: 18 | - Codeception\Extension\RunFailed 19 | - Codeception\Extension\Logger 20 | params: 21 | - .env.test.local # Should the necessary tokens to test the API -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tchapi/tuneefy", 3 | "description": "tuneefy is a unified way to share music with your friends, over various online music services", 4 | "license": "GPL-2.0", 5 | "authors": [ 6 | { 7 | "name": "tchapi", 8 | "email": "tchap@tchap.me", 9 | "homepage": "https://tchap.me" 10 | } 11 | ], 12 | "support": { 13 | "email": "team@tuneefy.com", 14 | "issues": "https://github.com/tchapi/tuneefy2/issues" 15 | }, 16 | "type": "project", 17 | "minimum-stability": "stable", 18 | "prefer-stable": true, 19 | "require": { 20 | "php": ">=8.2", 21 | "ext-ctype": "*", 22 | "ext-iconv": "*", 23 | "doctrine/dbal": "^3", 24 | "doctrine/doctrine-bundle": "^2.11", 25 | "doctrine/doctrine-migrations-bundle": "^3.3", 26 | "doctrine/orm": "^3.1", 27 | "league/oauth2-server-bundle": "^0.9.0", 28 | "nelmio/cors-bundle": "^2.4", 29 | "phpdocumentor/reflection-docblock": "^5.3", 30 | "phpstan/phpdoc-parser": "^1.26", 31 | "symfony/asset": "7.1.*", 32 | "symfony/asset-mapper": "7.1.*", 33 | "symfony/console": "7.1.*", 34 | "symfony/dotenv": "7.1.*", 35 | "symfony/expression-language": "7.1.*", 36 | "symfony/flex": "^2", 37 | "symfony/form": "7.1.*", 38 | "symfony/framework-bundle": "7.1.*", 39 | "symfony/http-client": "7.1.*", 40 | "symfony/intl": "7.1.*", 41 | "symfony/mailer": "7.1.*", 42 | "symfony/mime": "7.1.*", 43 | "symfony/monolog-bundle": "^3.0", 44 | "symfony/property-access": "7.1.*", 45 | "symfony/property-info": "7.1.*", 46 | "symfony/runtime": "7.1.*", 47 | "symfony/security-bundle": "7.1.*", 48 | "symfony/serializer": "7.1.*", 49 | "symfony/string": "7.1.*", 50 | "symfony/translation": "7.1.*", 51 | "symfony/twig-bundle": "7.1.*", 52 | "symfony/validator": "7.1.*", 53 | "symfony/web-link": "7.1.*", 54 | "symfony/yaml": "7.1.*", 55 | "twig/extra-bundle": "^2.12|^3.0", 56 | "twig/twig": "^2.12|^3.0" 57 | }, 58 | "config": { 59 | "allow-plugins": { 60 | "php-http/discovery": true, 61 | "symfony/flex": true, 62 | "symfony/runtime": true 63 | }, 64 | "sort-packages": true 65 | }, 66 | "autoload": { 67 | "psr-4": { 68 | "App\\": "src/" 69 | } 70 | }, 71 | "autoload-dev": { 72 | "psr-4": { 73 | "App\\Tests\\": "tests/" 74 | } 75 | }, 76 | "replace": { 77 | "symfony/polyfill-ctype": "*", 78 | "symfony/polyfill-iconv": "*", 79 | "symfony/polyfill-php80": "*", 80 | "symfony/polyfill-php81": "*", 81 | "symfony/polyfill-php82": "*" 82 | }, 83 | "scripts": { 84 | "auto-scripts": { 85 | "cache:clear": "symfony-cmd", 86 | "assets:install %PUBLIC_DIR%": "symfony-cmd", 87 | "importmap:install": "symfony-cmd" 88 | }, 89 | "post-install-cmd": [ 90 | "@auto-scripts" 91 | ], 92 | "post-update-cmd": [ 93 | "@auto-scripts" 94 | ] 95 | }, 96 | "conflict": { 97 | "symfony/symfony": "*" 98 | }, 99 | "require-dev": { 100 | "codeception/codeception": "^5.1", 101 | "codeception/module-asserts": "^3.0", 102 | "codeception/module-phpbrowser": "^3.0", 103 | "codeception/module-rest": "^3.3", 104 | "friendsofphp/php-cs-fixer": "^3.51", 105 | "symfony/browser-kit": "7.1.*", 106 | "symfony/css-selector": "7.1.*", 107 | "symfony/debug-bundle": "7.1.*", 108 | "symfony/maker-bundle": "^1.0", 109 | "symfony/phpunit-bridge": "7.1.*", 110 | "symfony/stopwatch": "7.1.*", 111 | "symfony/web-profiler-bundle": "7.1.*" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 6 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 7 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], 8 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 9 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 10 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 11 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 12 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 13 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 14 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 15 | League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true], 16 | ]; 17 | -------------------------------------------------------------------------------- /config/packages/asset_mapper.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | asset_mapper: 3 | # The paths to make available to the asset mapper. 4 | paths: 5 | - assets/ 6 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/debug.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | debug: 3 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 4 | # See the "server:dump" command to start a new server. 5 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 6 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(DATABASE_URL)%' 4 | 5 | # IMPORTANT: You MUST configure your server version, 6 | # either here or in the DATABASE_URL env var (see .env file) 7 | #server_version: '16' 8 | 9 | profiling_collect_backtrace: '%kernel.debug%' 10 | use_savepoints: true 11 | 12 | schema_filter: ~^(?!stats_|_mv)$~ 13 | orm: 14 | auto_generate_proxy_classes: true 15 | enable_lazy_ghost_objects: true 16 | report_fields_where_declared: true 17 | validate_xml_mapping: true 18 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 19 | auto_mapping: true 20 | controller_resolver: 21 | auto_mapping: false 22 | mappings: 23 | App: 24 | type: attribute 25 | is_bundle: false 26 | dir: '%kernel.project_dir%/src/Entity' 27 | prefix: 'App\Entity' 28 | alias: App 29 | 30 | when@test: 31 | doctrine: 32 | dbal: 33 | # "TEST_TOKEN" is typically set by ParaTest 34 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 35 | 36 | when@prod: 37 | doctrine: 38 | orm: 39 | auto_generate_proxy_classes: false 40 | proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' 41 | query_cache_driver: 42 | type: pool 43 | pool: doctrine.system_cache_pool 44 | result_cache_driver: 45 | type: pool 46 | pool: doctrine.result_cache_pool 47 | 48 | framework: 49 | cache: 50 | pools: 51 | doctrine.result_cache_pool: 52 | adapter: cache.app 53 | doctrine.system_cache_pool: 54 | adapter: cache.system 55 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 6 | enable_profiler: false 7 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | 6 | # Note that the session will be started ONLY if you read or write from it. 7 | session: 8 | name: tuneefy 9 | cookie_domain: .tuneefy.com 10 | 11 | #esi: true 12 | #fragments: true 13 | 14 | when@dev: 15 | framework: 16 | session: 17 | cookie_domain: '' 18 | 19 | when@test: 20 | framework: 21 | test: true 22 | session: 23 | storage_factory_id: session.storage.factory.mock_file 24 | -------------------------------------------------------------------------------- /config/packages/league_oauth2_server.yaml: -------------------------------------------------------------------------------- 1 | league_oauth2_server: 2 | authorization_server: 3 | private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%' 4 | private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%' 5 | encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%' 6 | enable_client_credentials_grant: true 7 | enable_password_grant: false 8 | enable_refresh_token_grant: false 9 | enable_auth_code_grant: false 10 | resource_server: 11 | public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%' 12 | scopes: 13 | available: ['api'] 14 | default: ['api'] 15 | persistence: 16 | doctrine: null 17 | 18 | when@test: 19 | league_oauth2_server: 20 | persistence: 21 | in_memory: null 22 | -------------------------------------------------------------------------------- /config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | -------------------------------------------------------------------------------- /config/packages/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: 3 | - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists 4 | 5 | when@dev: 6 | monolog: 7 | handlers: 8 | main: 9 | type: stream 10 | path: "%kernel.logs_dir%/%kernel.environment%.log" 11 | level: debug 12 | channels: ["!event"] 13 | # uncomment to get logging in your browser 14 | # you may have to allow bigger header sizes in your Web server configuration 15 | #firephp: 16 | # type: firephp 17 | # level: info 18 | #chromephp: 19 | # type: chromephp 20 | # level: info 21 | console: 22 | type: console 23 | process_psr_3_messages: false 24 | channels: ["!event", "!doctrine", "!console"] 25 | 26 | when@test: 27 | monolog: 28 | handlers: 29 | main: 30 | type: fingers_crossed 31 | action_level: error 32 | handler: nested 33 | excluded_http_codes: [404, 405] 34 | channels: ["!event"] 35 | nested: 36 | type: stream 37 | path: "%kernel.logs_dir%/%kernel.environment%.log" 38 | level: debug 39 | 40 | when@prod: 41 | monolog: 42 | handlers: 43 | main: 44 | type: fingers_crossed 45 | action_level: error 46 | handler: nested 47 | excluded_http_codes: [404, 405] 48 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 49 | nested: 50 | type: stream 51 | path: php://stderr 52 | level: debug 53 | formatter: monolog.formatter.json 54 | console: 55 | type: console 56 | process_psr_3_messages: false 57 | channels: ["!event", "!doctrine"] 58 | deprecation: 59 | type: stream 60 | channels: [deprecation] 61 | path: php://stderr 62 | formatter: monolog.formatter.json 63 | -------------------------------------------------------------------------------- /config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | paths: 3 | '^/api/': 4 | allow_origin: ['*'] 5 | allow_headers: ['*'] 6 | allow_methods: ['POST', 'GET'] 7 | max_age: 3600 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/packages/nyholm_psr7.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) 3 | Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' 4 | Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' 5 | Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' 6 | Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' 7 | Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' 8 | Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' 9 | 10 | nyholm.psr7.psr17_factory: 11 | class: Nyholm\Psr7\Factory\Psr17Factory 12 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 4 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 5 | #default_uri: http://localhost 6 | 7 | when@prod: 8 | framework: 9 | router: 10 | strict_requirements: null 11 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 3 | password_hashers: 4 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 5 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 6 | providers: 7 | admin_user_provider: 8 | id: App\Security\AdminUserProvider 9 | # api_client_provider: 10 | # id: App\Security\ApiClientProvider 11 | api_client_provider: 12 | entity: 13 | class: App\Entity\ApiClient 14 | property: oauth2ClientIdentifier 15 | firewalls: 16 | dev: 17 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 18 | security: false 19 | backend: 20 | pattern: ^/admin 21 | lazy: true 22 | custom_authenticators: 23 | - App\Security\LoginFormAuthenticator 24 | provider: admin_user_provider 25 | logout: 26 | path: admin_logout 27 | target: admin_dashboard 28 | api: 29 | pattern: ^/api 30 | security: "%api.use_oauth%" 31 | stateless: false # To allow bypassing via the session 32 | provider: api_client_provider 33 | oauth2: true 34 | 35 | # Note: Only the *first* access control that matches will be used 36 | access_control: 37 | - { path: ^/admin/login, roles: PUBLIC_ACCESS } 38 | - { path: ^/admin, roles: ROLE_ADMIN } 39 | - { path: ^/api/v2/auth/token, roles: PUBLIC_ACCESS } 40 | - { path: ^/api/v2/$, roles: PUBLIC_ACCESS } 41 | - { path: ^/api/v2/, roles: [ROLE_OAUTH2_API, ROLE_BYPASS_AUTH_API] } 42 | 43 | when@test: 44 | security: 45 | password_hashers: 46 | # By default, password hashers are resource intensive and take time. This is 47 | # important to generate secure password hashes. In tests however, secure hashes 48 | # are not important, waste resources and increase test times. The following 49 | # reduces the work factor to the lowest possible values. 50 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 51 | algorithm: auto 52 | cost: 4 # Lowest possible value for bcrypt 53 | time_cost: 3 # Lowest possible value for argon 54 | memory_cost: 10 # Lowest possible value for argon 55 | 56 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: fr 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - fr 7 | providers: 8 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | file_name_pattern: '*.twig' 3 | globals: 4 | ga_tracker_id: "%env(GA_TRACKER_ID)%" 5 | mail: 6 | captcha_key: "%mail.captcha_key%" 7 | 8 | when@test: 9 | twig: 10 | strict_variables: true 11 | 12 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | # Enables validator auto-mapping support. 4 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 5 | #auto_mapping: 6 | # App\Entity\: [] 7 | 8 | when@test: 9 | framework: 10 | validation: 11 | not_compromised_password: false 12 | -------------------------------------------------------------------------------- /config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | web_profiler: 3 | toolbar: true 4 | intercept_redirects: false 5 | 6 | framework: 7 | profiler: 8 | only_exceptions: false 9 | collect_serializer_data: true 10 | 11 | when@test: 12 | web_profiler: 13 | toolbar: false 14 | intercept_redirects: false 15 | 16 | framework: 17 | profiler: { collect: false } 18 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | > {{shared_path}}/logs/cron-job.log" 88 | cron_file: tuneefy_update-stats 89 | - name: Creates a cron file under /etc/cron.d for cleaning intents 90 | become: true 91 | become_user: root 92 | cron: 93 | name: "Tuneefy: Clean expired intents" 94 | minute: "18" 95 | user: "{{ansible_user}}" 96 | job: "/usr/bin/php {{current_path}}/bin/console tuneefy:clean-expired-intents >> {{shared_path}}/logs/cron-job.log" 97 | cron_file: tuneefy_clean-expired-intents 98 | - name: Creates a cron file under /etc/cron.d for cleaning expired tokens 99 | become: true 100 | become_user: root 101 | cron: 102 | name: "Tuneefy: Clear expired tokens" 103 | minute: "35" 104 | user: "{{ansible_user}}" 105 | job: "/usr/bin/php {{current_path}}/bin/console league:oauth2-server:clear-expired-tokens >> {{shared_path}}/logs/cron-job.log" 106 | cron_file: tuneefy_clean-expired-tokens 107 | - name: Update symlink 108 | file: 109 | src={{ release_path }} 110 | dest={{ current_path }} 111 | state=link 112 | - name: Restart PHP-FPM 113 | become: true 114 | become_user: root 115 | service: 116 | name: "php8.2-fpm" 117 | state: restarted 118 | - name: Give correct permissions 119 | file: 120 | state: directory 121 | path: "{{ current_path }}/var/cache" 122 | owner: debian 123 | group: www-data 124 | recurse: true 125 | mode: '0775' 126 | -------------------------------------------------------------------------------- /deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | server_name tuneefy.com; 4 | 5 | index index.php; 6 | root /home/tchap/www/tuneefy2/current/web; 7 | 8 | error_log /var/log/nginx/tuneefy2.error.log; 9 | set $app "tuneefy2"; 10 | access_log /var/log/nginx/all.access.log custom; 11 | 12 | location / { 13 | try_files $uri /index.php$is_args$args; 14 | } 15 | 16 | # Pass on to HHVM 17 | include php-fpm.conf; 18 | 19 | # Favicons and robots 20 | include favicon.robots.conf; 21 | 22 | # deny access to .htaccess files 23 | location ~ /\.ht { 24 | deny all; 25 | } 26 | 27 | } 28 | 29 | 30 | server { 31 | server_name data.tuneefy.com; 32 | 33 | index api.php; 34 | root /home/tchap/www/tuneefy2/current/web; 35 | 36 | error_log /var/log/nginx/api.tuneefy2.error.log; 37 | set $app "api-tuneefy2"; 38 | access_log /var/log/nginx/all.access.log custom; 39 | 40 | add_header 'Access-Control-Allow-Origin' "http://tuneefy.com"; 41 | add_header "Access-Control-Allow-Credentials" "true"; 42 | 43 | location / { 44 | 45 | # Preflighted requests 46 | if ($request_method = OPTIONS ) { 47 | add_header 'Access-Control-Allow-Origin' "http://tuneefy.com"; 48 | add_header "Access-Control-Allow-Credentials" "true"; 49 | add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD"; 50 | add_header "Access-Control-Allow-Headers" "X-Requested-With"; 51 | return 200; 52 | } 53 | 54 | try_files $uri /api.php$is_args$args; 55 | } 56 | 57 | # Pass on to HHVM 58 | include php-fpm.conf; 59 | 60 | # Favicons and robots 61 | include favicon.robots.conf; 62 | 63 | # deny access to .htaccess files 64 | location ~ /\.ht { 65 | deny all; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /deploy/vars.yml.dist: -------------------------------------------------------------------------------- 1 | tuneefy_hosts: frontend 2 | project_path: /var/www/tuneefy -------------------------------------------------------------------------------- /examples/postman/tuneefy.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "18a1dd41-4a62-a88e-208a-d0ac5ef4d3dd", 3 | "name": "tuneefy credentials", 4 | "values": [ 5 | { 6 | "enabled": true, 7 | "key": "api_key", 8 | "value": "YOUR_API_KEY", 9 | "type": "text" 10 | }, 11 | { 12 | "enabled": true, 13 | "key": "api_secret", 14 | "value": "YOUR_API_SECRET", 15 | "type": "text" 16 | }, 17 | { 18 | "enabled": true, 19 | "key": "api_endpoint", 20 | "value": "https://data.tuneefy.com/v2", 21 | "type": "text" 22 | }, 23 | { 24 | "enabled": true, 25 | "key": "example_intent", 26 | "value": "5912c9aa3e606", 27 | "type": "text" 28 | }, 29 | { 30 | "enabled": true, 31 | "key": "search_type_track", 32 | "value": "track", 33 | "type": "text" 34 | }, 35 | { 36 | "enabled": true, 37 | "key": "search_type_album", 38 | "value": "album", 39 | "type": "text" 40 | }, 41 | { 42 | "enabled": true, 43 | "key": "example_platform_tag", 44 | "value": "spotify", 45 | "type": "text" 46 | }, 47 | { 48 | "enabled": true, 49 | "key": "search_example_platform", 50 | "value": "qobuz", 51 | "type": "text" 52 | } 53 | ], 54 | "timestamp": 1494403811262, 55 | "_postman_variable_scope": "environment", 56 | "_postman_exported_at": "2017-05-10T16:09:39.983Z", 57 | "_postman_exported_using": "Postman/4.10.7" 58 | } -------------------------------------------------------------------------------- /examples/search.curl.php: -------------------------------------------------------------------------------- 1 | token_type) && $token->token_type === 'Bearer') { 21 | $br = curl_init($searchEndpoint); 22 | curl_setopt($br, CURLOPT_HTTPHEADER, [ 23 | 'Authorization: Bearer '.$token->access_token, 24 | 'Accept: application/json', 25 | ]); 26 | curl_setopt($br, CURLOPT_RETURNTRANSFER, true); 27 | $data = curl_exec($br); 28 | curl_close($br); 29 | 30 | // 3. Tada ! 31 | echo "🎉\n"; 32 | var_dump($data); 33 | } else { 34 | echo "Wrong key/secret pair"; 35 | } 36 | -------------------------------------------------------------------------------- /examples/search.curl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | response=$(curl -X POST --silent -d client_id=administrator -d client_secret=password -d grant_type=client_credentials https://data.tuneefy.com/v2/auth/token) 4 | 5 | token=$(echo $response | jq --raw-output '.access_token') 6 | 7 | echo 🎉 8 | curl --header "Authorization: Bearer $token" "https://data.tuneefy.com/v2/search/track/spotify?q=amon+tobin&limit=1" 9 | -------------------------------------------------------------------------------- /examples/search.request.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | var key = 'administrator'; 4 | var secret = 'password'; 5 | 6 | var tokenEndpoint = 'https://data.tuneefy.com/v2/auth/token'; 7 | var searchEndpoint = 'https://data.tuneefy.com/v2/search/track/spotify?q=amon+tobin&limit=1'; 8 | 9 | // 1. Request token 10 | request.post({ 11 | url: tokenEndpoint, 12 | form: { 13 | 'grant_type': 'client_credentials', 14 | 'client_id': key, 15 | 'client_secret': secret 16 | } 17 | }, function(err, httpResponse, body) { 18 | var json = JSON.parse(body); 19 | 20 | if (json.token_type && json.token_type === 'Bearer') { 21 | // 2. Use token for search on Spotify 22 | request({ 23 | url: searchEndpoint, 24 | 'auth': { 25 | 'bearer': json.access_token 26 | }, 27 | method: 'GET' 28 | }, function(err, httpResponse, body) { 29 | var json = JSON.parse(body); 30 | 31 | // 3. Tada ! 32 | console.log("🎉"); 33 | console.log(JSON.stringify(json, null, 4)); 34 | }); 35 | } 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /examples/search.requests.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import requests, json 3 | 4 | key = 'administrator' 5 | secret = 'password' 6 | 7 | tokenEndpoint = 'https://data.tuneefy.com/v2/auth/token' 8 | searchEndpoint = 'https://data.tuneefy.com/v2/search/track/spotify?q=amon+tobin&limit=1' 9 | 10 | # 1. Request token 11 | payload = {'grant_type': 'client_credentials', 'client_id': key, 'client_secret': secret} 12 | req = requests.post(tokenEndpoint,data=payload) 13 | 14 | token = req.json() 15 | 16 | # 2. Use token for search on Spotify 17 | if token['token_type'] and token['token_type'] == 'Bearer': 18 | headers = {'Authorization': 'Bearer '+token['access_token'], 'Accept': 'application/json'} 19 | req = requests.get(searchEndpoint, headers=headers) 20 | 21 | # 3. Tada ! 22 | print "🎉" 23 | print json.dumps(req.json(), indent=4, separators=(',', ': ')) 24 | else: 25 | print "Wrong key/secret pair" 26 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const uglify = require('gulp-uglify'); 3 | const sass = require('gulp-sass')(require('sass')); 4 | const concat = require('gulp-concat'); 5 | const pump = require('pump'); 6 | 7 | const resourcesFolder = 'assets/'; 8 | const buildFolder = 'public/build/' 9 | 10 | const log = (error) => { 11 | if (error) { 12 | console.log(error); 13 | } 14 | } 15 | 16 | gulp.task('javascript', function (done) { 17 | pump([ 18 | gulp.src(resourcesFolder + 'js/**/*.js'), 19 | uglify(), 20 | gulp.dest(buildFolder + 'js') 21 | ], 22 | log, 23 | done 24 | ); 25 | }); 26 | 27 | gulp.task('twig', function (done) { 28 | pump([ 29 | gulp.src(resourcesFolder + 'js/**/*.twig'), 30 | gulp.dest(buildFolder + 'js') 31 | ], 32 | log, 33 | done 34 | ); 35 | }); 36 | 37 | gulp.task('sass', function (done) { 38 | pump([ 39 | gulp.src(resourcesFolder + 'scss/styles.scss'), 40 | sass({ outputStyle: 'compressed' }).on('error', sass.logError), 41 | gulp.dest(buildFolder + 'css') 42 | ], 43 | log, 44 | done 45 | ); 46 | }); 47 | 48 | gulp.task('embed', function (done) { 49 | pump([ 50 | gulp.src(resourcesFolder + 'scss/partials/embed.scss'), 51 | sass({ outputStyle: 'compressed' }).on('error', sass.logError), 52 | gulp.dest(buildFolder + 'css') 53 | ], 54 | log, 55 | done 56 | ); 57 | }); 58 | 59 | gulp.task('widget', function (done) { 60 | pump([ 61 | gulp.src([resourcesFolder + 'scss/partials/reset.scss', resourcesFolder + 'widget/widget.scss']), 62 | concat('widget.scss'), 63 | sass({ outputStyle: 'compressed' }).on('error', sass.logError), 64 | gulp.dest(buildFolder + 'css') 65 | ], 66 | log 67 | ); 68 | pump([ 69 | gulp.src(resourcesFolder + 'widget/widget-overlay.scss'), 70 | sass({ outputStyle: 'compressed' }).on('error', sass.logError), 71 | gulp.dest(buildFolder + 'css') 72 | ], 73 | log 74 | ); 75 | pump([ 76 | gulp.src(resourcesFolder + 'widget/widget.js'), 77 | uglify(), 78 | gulp.dest(buildFolder + 'js') 79 | ], 80 | log, 81 | done 82 | ); 83 | }); 84 | 85 | gulp.task('default', gulp.parallel('widget', 'embed', 'sass', 'javascript', 'twig')); 86 | -------------------------------------------------------------------------------- /migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/migrations/.gitignore -------------------------------------------------------------------------------- /migrations/Version20240401205417.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE oauth2_access_token (identifier CHAR(80) NOT NULL, client VARCHAR(32) NOT NULL, expiry DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', user_identifier VARCHAR(128) DEFAULT NULL, scopes TEXT DEFAULT NULL COMMENT \'(DC2Type:oauth2_scope)\', revoked TINYINT(1) NOT NULL, INDEX IDX_454D9673C7440455 (client), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 20 | $this->addSql('CREATE TABLE oauth2_authorization_code (identifier CHAR(80) NOT NULL, client VARCHAR(32) NOT NULL, expiry DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', user_identifier VARCHAR(128) DEFAULT NULL, scopes TEXT DEFAULT NULL COMMENT \'(DC2Type:oauth2_scope)\', revoked TINYINT(1) NOT NULL, INDEX IDX_509FEF5FC7440455 (client), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 21 | $this->addSql('CREATE TABLE oauth2_client (identifier VARCHAR(32) NOT NULL, name VARCHAR(128) NOT NULL, secret VARCHAR(128) DEFAULT NULL, redirect_uris TEXT DEFAULT NULL COMMENT \'(DC2Type:oauth2_redirect_uri)\', grants TEXT DEFAULT NULL COMMENT \'(DC2Type:oauth2_grant)\', scopes TEXT DEFAULT NULL COMMENT \'(DC2Type:oauth2_scope)\', active TINYINT(1) NOT NULL, allow_plain_text_pkce TINYINT(1) DEFAULT 0 NOT NULL, PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 22 | $this->addSql('CREATE TABLE oauth2_refresh_token (identifier CHAR(80) NOT NULL, access_token CHAR(80) DEFAULT NULL, expiry DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', revoked TINYINT(1) NOT NULL, INDEX IDX_4DD90732B6A2DD68 (access_token), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 23 | $this->addSql('ALTER TABLE oauth2_access_token ADD CONSTRAINT FK_454D9673C7440455 FOREIGN KEY (client) REFERENCES oauth2_client (identifier) ON DELETE CASCADE'); 24 | $this->addSql('ALTER TABLE oauth2_authorization_code ADD CONSTRAINT FK_509FEF5FC7440455 FOREIGN KEY (client) REFERENCES oauth2_client (identifier) ON DELETE CASCADE'); 25 | $this->addSql('ALTER TABLE oauth2_refresh_token ADD CONSTRAINT FK_4DD90732B6A2DD68 FOREIGN KEY (access_token) REFERENCES oauth2_access_token (identifier) ON DELETE SET NULL'); 26 | } 27 | 28 | public function down(Schema $schema): void 29 | { 30 | $this->addSql('ALTER TABLE oauth2_access_token DROP FOREIGN KEY FK_454D9673C7440455'); 31 | $this->addSql('ALTER TABLE oauth2_authorization_code DROP FOREIGN KEY FK_509FEF5FC7440455'); 32 | $this->addSql('ALTER TABLE oauth2_refresh_token DROP FOREIGN KEY FK_4DD90732B6A2DD68'); 33 | $this->addSql('DROP TABLE oauth2_access_token'); 34 | $this->addSql('DROP TABLE oauth2_authorization_code'); 35 | $this->addSql('DROP TABLE oauth2_client'); 36 | $this->addSql('DROP TABLE oauth2_refresh_token'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tuneefy", 3 | "version": "2.1.0", 4 | "author": "tchap", 5 | "license": "ISC", 6 | "description": "A new version of [tuneefy](http://tuneefy.com) built for PHP 7, from the ground up, using the minimal Slim framework and a few helper libraries.", 7 | "main": "index.js", 8 | "directories": { 9 | "test": "tests" 10 | }, 11 | "scripts": { 12 | "build": "gulp", 13 | "api-documentation": "./node_modules/aglio/bin/aglio.js --theme-full-width --theme-template triple --theme-variables streak --theme-style default --theme-style templates/api/api.less --no-theme-condense -i templates/api/main.apib -o templates/api.html" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/tchapi/tuneefy2.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/tchapi/tuneefy2/issues" 21 | }, 22 | "homepage": "https://github.com/tchapi/tuneefy2#readme", 23 | "devDependencies": { 24 | "aglio": "^2.3.0", 25 | "gulp": "^4.0.2", 26 | "gulp-concat": "^2.6.1", 27 | "gulp-sass": "^5.1.0", 28 | "gulp-uglify": "^3.0.2", 29 | "pump": "^3.0", 30 | "sass": "^1.54.9", 31 | "standard": "^17.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/build/css/embed.css: -------------------------------------------------------------------------------- 1 | body{font-size:.7em}#footer-wrapper,#header-wrapper{display:none}.wrap{width:auto;margin:0}.wrap.bdBot,#trackInfo.bdBot{border:none}.Bcolor{background:rgba(0,0,0,0)}#wrapper{background:#151515;border-radius:3px;position:relative;box-shadow:0 1px #070707 inset,0 1px #333}#tagline{height:36px;padding:0}h1.logo{display:inline-block;margin-left:-6px;margin-top:-6px}h1.logo>img{width:100px;height:auto}p.tagline{float:right;padding-top:12px;padding-right:10px;font-size:1em;color:dimgray}#content{padding:0}#mainTitle{font-size:1.7em;text-align:center;padding-top:50px}#mainContent{width:100%;margin:0}#trackInfo{height:90px;padding:8px}#trackInfo .cover{float:left;position:relative;margin:0 8px 0 0}#trackInfo .coverlay{width:80px;height:80px}#trackInfo .info{margin:0}#trackInfo .infoContent{vertical-align:middle}#trackInfo .title{font-weight:bold;font-size:1.5em;line-height:.9em;color:#a2a2a2;padding-top:4px}#trackInfo .artist{font-size:1.2em;line-height:1.3em;color:#474747;padding-top:0}#trackInfo .album{font-size:.75em;color:#474747;text-transform:uppercase}#platforms,#share{padding:0px}#platforms{padding-top:1px}#platforms .listenTitle,#share .mainTitle{text-transform:uppercase;font-size:.75em;color:#aaa;margin-bottom:5px}#platforms .btns_full{margin-right:3px;margin-bottom:0px}#linkHolder,#externalPlatformsActions{background:#1f1f1f;float:left;border:1px solid #000;padding:8px;position:relative;border-radius:3px}#linkHolder{margin-right:10px}.newWindow{opacity:.9;margin-left:3px}@media(max-width: 500px){h1.logo{width:auto}#tagline{text-align:left}#tagline p{float:right;padding-top:12px;padding-right:10px}#trackInfo{height:auto;overflow:auto}#trackInfo .info{width:auto}}@media(max-width: 310px){#tagline{height:auto}#tagline p{display:block;padding:0;float:none;margin:0 0 8px 12px}} -------------------------------------------------------------------------------- /public/build/css/widget-overlay.css: -------------------------------------------------------------------------------- 1 | div#tuneefy_overlay{position:fixed;display:block;width:450px;height:180px;top:0;right:-2px;background:#1c1c1c;border:1px solid #000;border-radius:0 0 0 4px;box-shadow:0 0 6px #000;z-index:1000 !important}#tuneefy_overlay div.closeButton{position:absolute;display:block;top:10px;right:11px;background:url("//tuneefy.com/img/close.png") no-repeat scroll center top rgba(0,0,0,0);height:32px;width:32px;cursor:pointer}#tuneefy_overlay div.closeButton:hover{background-position:0px -32px}#tuneefy_overlay iframe{border:none;background:rgba(0,0,0,0);height:100%;width:100%}#tuneefy_overlay .middleBox{position:relative;top:66px;left:200px} -------------------------------------------------------------------------------- /public/build/css/widget.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}a,a:link{text-decoration:none}input,textarea{outline:none}body{font:normal normal normal 1em/1em arial,helvetica,sans-serif}a.btn{position:relative;color:#fff;text-transform:lowercase;font-size:14px;font-weight:bold;display:block;background:#222;background-image:-moz-linear-gradient(center top, #262626 0%, #1B1B1B 100%);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0, #262626), color-stop(1, #1B1B1B));border:1px solid #000;border-radius:3px;padding:6px 15px 7px 15px;cursor:pointer;text-shadow:0 -2px #000;box-shadow:0 1px #333 inset,0 1px #171717}a.btn:hover{color:#76bad0}a.btn:active{box-shadow:0 3px 3px #171717 inset,0 1px #171717}a.btns{display:inline-block;width:36px;height:31px;cursor:pointer}a.btns:hover{background-position:0px -32px}a.btns.off{background-position:0px -64px}a.btn_deezer{background:url("../img/platforms/platform_deezer.png") no-repeat}a.btn_spotify{background:url("../img/platforms/platform_spotify.png") no-repeat}a.btn_qobuz{background:url("../img/platforms/platform_qobuz.png") no-repeat}a.btn_lastfm{background:url("../img/platforms/platform_lastfm.png") no-repeat}a.btn_itunes{background:url("../img/platforms/platform_itunes.png") no-repeat}a.btn_youtube{background:url("../img/platforms/platform_youtube.png") no-repeat}a.btn_soundcloud{background:url("../img/platforms/platform_soundcloud.png") no-repeat}a.btn_mixcloud{background:url("../img/platforms/platform_mixcloud.png") no-repeat}a.btn_groove{background:url("../img/platforms/platform_groove.png") no-repeat}a.btn_napster{background:url("../img/platforms/platform_napster.png") no-repeat}a.btn_tidal{background:url("../img/platforms/platform_tidal.png") no-repeat}a.btn_amazon{background:url("../img/platforms/platform_amazon.png") no-repeat}a.btn_hypem{background:url("../img/platforms/platform_hypem.png") no-repeat}.txtS{text-shadow:0 -1px #000 !important}ul li.tHeader{display:none}#waiting{background:#1a1a1a;height:100%;left:0;opacity:.8;position:absolute;top:0;width:100%;z-index:10}#waiting img{position:absolute;left:200px;top:66px}#results{padding:16px}.nbResults{color:#76bad0;font-size:18px;text-shadow:0 -1px #000;margin-bottom:10px}li.tResult{box-shadow:0 1px #070707 inset,0 1px #333;border-radius:3px;background:#151515;height:60px;padding:10px}li.tResult .coverlay{position:absolute;top:4px;left:4px;width:48px;height:48px;border:1px solid #aaa;background:url("../img/coverlay.png") center center}li.tResult .tImage{float:left;position:relative;margin-right:20px}li.tResult .tImage img{width:50px;height:50px;border:4px solid #fff;box-shadow:0px 0px 3px #000;display:block}li.tResult .tTitle{font-weight:bold;font-size:16px;color:#a2a2a2;padding-top:4px}li.tResult .tFeat{display:none}li.tResult .tArtist{color:#474747;font-size:12px;padding-top:4px}li.tResult .tAlbum{color:#474747;font-size:10px;padding-top:2px;text-transform:uppercase}li.tResult .tShare{position:absolute;bottom:14px;right:138px}.tLinks{position:absolute;bottom:11px;left:14px}.tLinks .wrapper{height:33px;overflow:hidden;width:205px}#more{position:absolute;bottom:14px;right:16px}#alerts{height:100%;width:100%;text-align:center;padding-top:66px}#alerts span{text-shadow:0 -1px #000 !important;color:#76bad0}a.backToTop{display:none !important} -------------------------------------------------------------------------------- /public/build/js/about.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){var r=$("ul.platformsPatterns li.platform");$("form.contactForm #send").click(function(){var a=$("form.contactForm #email"),r=$("form.contactForm #message"),o=$("form.contactForm #g-recaptcha-response"),t=$("form").attr("action");return r.removeClass("error"),a.removeClass("error"),""==a.val()?(a.addClass("error"),!1):""==r.val()?(r.addClass("error"),!1):($("form.contactForm").hide(),$(".waitingMail").show(),void $.post(t,{mail:a.val(),message:r.val(),captcha:o.val()},function(a){$(".waitingMail").hide(),("1"==a?$(".successMail"):$(".errorMail")).show()}))}),r.click(function(a){$(a.target).hasClass("platform")&&(r.removeClass("active"),$(a.target).addClass("active"),r.find("ul").hide(),$(a.target).find("ul").show())})}); -------------------------------------------------------------------------------- /public/build/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){$("#lang span").click(function(o){o="tuneefyLocale="+$(o.target).attr("lang")+"; ";o+="expires=Sat, 01 Feb 2042 01:20:42 GMT; path=/; domain= "+$DOMAIN+";",document.cookie=o,location.reload()}),$(document).on("click",".backToTop",function(o){o.preventDefault(),$("html,body").animate({scrollTop:0},1500)})}); -------------------------------------------------------------------------------- /public/build/js/show.js: -------------------------------------------------------------------------------- 1 | function toggleEmbed(){$("#embedHolder").toggle(),$("#embed").toggleClass("open")}function newTweet(t){var e=($(window).width()-575)/2,n=($(window).height()-400)/2;window.open("https://twitter.com/home?status="+t,"twitter","status=1,width=575,height=400,top="+n+",left="+e)}$(document).ready(function(){$("#mainLink, #embedContent").click(function(t){$(t.target).focus(),$(t.target).select()})}); -------------------------------------------------------------------------------- /public/build/js/trends.js: -------------------------------------------------------------------------------- 1 | Raphael.fn.pieChart=function(M,$,g,v,x,t){var y=this,C=Math.PI/180,k=this.set();function a(t){var a,e,o,i,n,s,h,r,p,c,l=360*v[t]/z,f=R+l/2,u=500,m=(a=M,e=$,o=g,n=(i=R)+l,s={fill:Raphael.color("#"+x[t][2]),stroke:"none"},h=a+o*Math.cos(-i*C),r=a+o*Math.cos(-n*C),p=e+o*Math.sin(-i*C),c=e+o*Math.sin(-n*C),y.path(["M",a,e,"L",h,p,"A",o,o,0,+(180 2 |
    3 | {%- if type == "track" -%} 4 | 5 | {%- else -%} 6 | 7 | {%- endif -%} 8 |
    9 |
    10 | {%- if type == "track" -%} 11 |
    12 |
    {{ item.safe_title }} 13 |
    14 | {%- if item.extra_info.is_cover or item.album.extra_info.is_cover -%} 15 | [Cover]  16 | {%- endif -%} 17 | {%- if item.extra_info.is_remix -%} 18 | [Remix]  19 | {%- endif -%} 20 | {%- if item.extra_info.acoustic -%} 21 | [Acoustic]  22 | {%- endif -%} 23 |
    24 |
    25 |
    26 |
    27 |
    {{ item.album.artist }}
    28 |
    29 |
    30 |
    {{ item.album.safe_title }}
    31 |
    32 | {%- else -%} 33 |
    34 |
    {{ item.safe_title }} 35 |
    36 | {%- if item.extra_info.is_cover -%} 37 | [Cover]  38 | {%- endif -%} 39 | {%- if item.extra_info.is_remix -%} 40 | [Remix]  41 | {%- endif -%} 42 | {%- if item.extra_info.acoustic -%} 43 | [Acoustic]  44 | {%- endif -%} 45 |
    46 |
    47 |
    48 |
    49 |
    {{ item.artist }}
    50 |
    51 | {%- endif -%} 52 | 65 | {{ share }} 66 | -------------------------------------------------------------------------------- /public/build/js/vendor/tablesort.js: -------------------------------------------------------------------------------- 1 | !function(u){u.tablesort=function(t,e){var s=this;this.$table=t,this.$thead=this.$table.find("thead"),this.settings=u.extend({},u.tablesort.defaults,e),this.$sortCells=(0'),l=(e.innerHTML=n+(''),e.style.opacity=1,encodeURIComponent(decodeURIComponent(document.location.href))),t=null,i=null,a="";try{-1!=l.indexOf(".deezer.")&&null!=el("player_track_title")?(i=el("player_track_artist").innerHTML,t=el("player_track_title").innerHTML):-1!=l.indexOf(".deezer.")&&null!=elcl("player-track-title")?(i=elcl("player-track-artist")[0].childNodes[1].innerHTML,t=elcl("player-track-title")[0].firstChild.innerHTML):-1!=l.indexOf(".grooveshark.")&&null!=el("now-playing-metadata")?(i=el("now-playing-metadata").childNodes[4].innerHTML,t=el("now-playing-metadata").firstChild.innerHTML):-1!=l.indexOf(".radionomy.")&&null!=el("track-name")?(i=el("artist-name").innerHTML,t=el("track-name").innerHTML):-1!=l.indexOf(".stereomood.")&&null!=el("info_track_title")?(i=el("info_track_artist").innerHTML,t=el("info_track_title").innerHTML):-1!=l.indexOf(".musicmaze.")&&null!=el("song-title")?(i=el("artist-name").firstChild.innerHTML,t=el("song-title").firstChild.innerHTML):-1!=l.indexOf(".myspace.com/music/player")&&null!=el("mainContent")?(i=el("mainContent").childNodes[3].childNodes[11].childNodes[1].childNodes[5].firstChild.innerHTML,t=el("mainContent").childNodes[3].childNodes[11].childNodes[1].childNodes[3].firstChild.innerHTML):-1!=l.indexOf(".myspace.")&&null!=document.getElementsByTagName("h1")[1]?(i=document.getElementsByTagName("h1")[1].firstChild.innerHTML,t=document.getElementsByTagName("h6")[0].firstChild.firstChild.innerHTML):-1!=l.indexOf("player.qobuz.")&&null!=el("now-playing")?l=encodeURIComponent(el("now-playing").childNodes[2].childNodes[3].childNodes[0].href):-1!=l.indexOf("music.xbox.")&&null!=el("player")?(t=$("#player").find(".playerNowPlaying .playerNowPlayingMetadata .primaryMetadata a").html(),i=$("#player").find(".playerNowPlaying .playerNowPlayingMetadata .secondaryMetadata a:first-child").html()):-1!=l.indexOf("radiooooo.")&&null!=elcl("songinfo--box")[0]&&(i=elcl("song__artist")[0].innerHTML,t=elcl("song__title")[0].innerHTML)}catch(e){t=i=null}null!=i&&void 0!==i&&(a+=i+"+"),null!=t&&void 0!==t&&(a+=t);a=""==a?l:encodeURIComponent(a);e.innerHTML=n+(''),document.body.appendChild(e)}(); -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/favicon.png -------------------------------------------------------------------------------- /public/img/ajax-loader-widget.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/ajax-loader-widget.gif -------------------------------------------------------------------------------- /public/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/ajax-loader.gif -------------------------------------------------------------------------------- /public/img/album_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/album_link.png -------------------------------------------------------------------------------- /public/img/album_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/album_off.png -------------------------------------------------------------------------------- /public/img/album_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/album_on.png -------------------------------------------------------------------------------- /public/img/artist_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/artist_link.png -------------------------------------------------------------------------------- /public/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/background.png -------------------------------------------------------------------------------- /public/img/background_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/background_menu.png -------------------------------------------------------------------------------- /public/img/browsers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/browsers.png -------------------------------------------------------------------------------- /public/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/close.png -------------------------------------------------------------------------------- /public/img/coverlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/coverlay.png -------------------------------------------------------------------------------- /public/img/creator_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/creator_design.png -------------------------------------------------------------------------------- /public/img/creator_idea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/creator_idea.png -------------------------------------------------------------------------------- /public/img/dashboard/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/dashboard/album.png -------------------------------------------------------------------------------- /public/img/dashboard/song-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/dashboard/song-alt.png -------------------------------------------------------------------------------- /public/img/dashboard/song.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/dashboard/song.png -------------------------------------------------------------------------------- /public/img/dashboard/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/dashboard/users.png -------------------------------------------------------------------------------- /public/img/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/error.png -------------------------------------------------------------------------------- /public/img/extra_acoustic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/extra_acoustic.png -------------------------------------------------------------------------------- /public/img/extra_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/extra_cover.png -------------------------------------------------------------------------------- /public/img/extra_remix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/extra_remix.png -------------------------------------------------------------------------------- /public/img/fact_free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/fact_free.png -------------------------------------------------------------------------------- /public/img/fact_friends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/fact_friends.png -------------------------------------------------------------------------------- /public/img/fact_patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/fact_patterns.png -------------------------------------------------------------------------------- /public/img/fact_pertinence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/fact_pertinence.png -------------------------------------------------------------------------------- /public/img/fact_picks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/fact_picks.png -------------------------------------------------------------------------------- /public/img/fact_sharing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/fact_sharing.png -------------------------------------------------------------------------------- /public/img/fact_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/fact_widget.png -------------------------------------------------------------------------------- /public/img/home_pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/home_pic.png -------------------------------------------------------------------------------- /public/img/icon_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/icon_search.png -------------------------------------------------------------------------------- /public/img/icon_search_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/icon_search_help.png -------------------------------------------------------------------------------- /public/img/iphone-style/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/iphone-style/off.png -------------------------------------------------------------------------------- /public/img/iphone-style/on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/iphone-style/on.png -------------------------------------------------------------------------------- /public/img/iphone-style/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/iphone-style/slider.png -------------------------------------------------------------------------------- /public/img/iphone-style/slider_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/iphone-style/slider_center.png -------------------------------------------------------------------------------- /public/img/iphone-style/slider_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/iphone-style/slider_left.png -------------------------------------------------------------------------------- /public/img/iphone-style/slider_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/iphone-style/slider_right.png -------------------------------------------------------------------------------- /public/img/lang_underlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/lang_underlay.png -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/logo.png -------------------------------------------------------------------------------- /public/img/logo_footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/logo_footer.png -------------------------------------------------------------------------------- /public/img/more_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/more_options.png -------------------------------------------------------------------------------- /public/img/new_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/new_window.png -------------------------------------------------------------------------------- /public/img/nothumb_album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/nothumb_album.png -------------------------------------------------------------------------------- /public/img/nothumb_track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/nothumb_track.png -------------------------------------------------------------------------------- /public/img/options_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/options_down.png -------------------------------------------------------------------------------- /public/img/pagination_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/pagination_left.png -------------------------------------------------------------------------------- /public/img/pagination_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/pagination_right.png -------------------------------------------------------------------------------- /public/img/platforms/platform_amazon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_amazon.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_amazon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_amazon.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_deezer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_deezer.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_groove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_groove.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_hypem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_hypem.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_itunes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_itunes.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_lastfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_lastfm.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_mixcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_mixcloud.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_napster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_napster.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_qobuz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_qobuz.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_soundcloud.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_spotify.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_tidal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_tidal.png -------------------------------------------------------------------------------- /public/img/platforms/platform_chart_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_chart_youtube.png -------------------------------------------------------------------------------- /public/img/platforms/platform_deezer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_deezer.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_amazon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_amazon.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_deezer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_deezer.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_groove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_groove.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_hypem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_hypem.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_itunes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_itunes.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_lastfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_lastfm.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_mixcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_mixcloud.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_napster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_napster.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_qobuz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_qobuz.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_soundcloud.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_spotify.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_tidal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_tidal.png -------------------------------------------------------------------------------- /public/img/platforms/platform_full_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_full_youtube.png -------------------------------------------------------------------------------- /public/img/platforms/platform_groove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_groove.png -------------------------------------------------------------------------------- /public/img/platforms/platform_hypem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_hypem.png -------------------------------------------------------------------------------- /public/img/platforms/platform_itunes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_itunes.png -------------------------------------------------------------------------------- /public/img/platforms/platform_lastfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_lastfm.png -------------------------------------------------------------------------------- /public/img/platforms/platform_mixcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_mixcloud.png -------------------------------------------------------------------------------- /public/img/platforms/platform_napster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_napster.png -------------------------------------------------------------------------------- /public/img/platforms/platform_qobuz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_qobuz.png -------------------------------------------------------------------------------- /public/img/platforms/platform_soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_soundcloud.png -------------------------------------------------------------------------------- /public/img/platforms/platform_spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_spotify.png -------------------------------------------------------------------------------- /public/img/platforms/platform_tidal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_tidal.png -------------------------------------------------------------------------------- /public/img/platforms/platform_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_youtube.png -------------------------------------------------------------------------------- /public/img/platforms/platform_youtube_compliance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/platforms/platform_youtube_compliance.png -------------------------------------------------------------------------------- /public/img/plus_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/plus_options.png -------------------------------------------------------------------------------- /public/img/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/reset.png -------------------------------------------------------------------------------- /public/img/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/share.png -------------------------------------------------------------------------------- /public/img/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/social.png -------------------------------------------------------------------------------- /public/img/song_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/song_link.png -------------------------------------------------------------------------------- /public/img/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/success.png -------------------------------------------------------------------------------- /public/img/track_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/track_off.png -------------------------------------------------------------------------------- /public/img/track_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/track_on.png -------------------------------------------------------------------------------- /public/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/twitter.png -------------------------------------------------------------------------------- /public/img/widget_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/tuneefy2/e51a5c3390c3e55fec1336d22dd178a43c4cf9d6/public/img/widget_button.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | itemRepository->cleanExpiredIntents(); 25 | 26 | return Command::SUCCESS; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Command/PlatformIpsFetcherCommand.php: -------------------------------------------------------------------------------- 1 | $value) { 30 | $ips = gethostbynamel($value); 31 | echo " '".$value.':443:'.$ips[0]."',\n"; 32 | } 33 | echo "];\n"; 34 | 35 | return Command::SUCCESS; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Command/StatsUpdaterCommand.php: -------------------------------------------------------------------------------- 1 | statsService->updateMaterializedViews(); 23 | 24 | return Command::SUCCESS; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Controller/BackendController.php: -------------------------------------------------------------------------------- 1 | getApiStats(); 26 | $clients = $entityManager->getRepository(ApiClient::class)->findAllWithOAuth2Client(); 27 | $activeClients = array_filter($clients, function ($e) {return $e['active']; }); 28 | $stats = []; 29 | 30 | foreach ($statsRaw as $stat) { 31 | if (isset($stats[StatsService::METHOD_NAMES[$stat['method']]])) { 32 | $stats[StatsService::METHOD_NAMES[$stat['method']]] += $stat['count']; 33 | } else { 34 | $stats[StatsService::METHOD_NAMES[$stat['method']]] = $stat['count']; 35 | } 36 | } 37 | 38 | // Format 39 | foreach ($stats as $key => $stat) { 40 | if ($stat > 1000000) { 41 | $stats[$key] = number_format($stat / 1000000, 2, ',', ' ').' M'; 42 | } elseif ($stat > 1000) { 43 | $stats[$key] = number_format($stat / 1000, 0, ',', ' ').' k'; 44 | } else { 45 | $stats[$key] = number_format($stat, 0, ',', ' '); 46 | } 47 | } 48 | 49 | return $this->render('admin/dashboard.html.twig', [ 50 | 'itemsStats' => $statsService->getItemsStats(), 51 | 'apiStats' => $stats, 52 | 'clients' => $clients, 53 | 'activeClients' => $activeClients, 54 | ]); 55 | } 56 | 57 | #[Route('/api/clients', name: 'clients')] 58 | public function clients(StatsService $statsService, EntityManagerInterface $entityManager): Response 59 | { 60 | $clients = $entityManager->getRepository(ApiClient::class)->findAllWithOAuth2Client(); 61 | $statsRaw = $statsService->getApiStats(); 62 | 63 | $stats = []; 64 | foreach ($statsRaw as $stat) { 65 | $stats[$stat['client_id']][] = [ 66 | 'method' => StatsService::METHOD_NAMES[$stat['method']], 67 | 'count' => $stat['count'], 68 | ]; 69 | } 70 | 71 | return $this->render('admin/clients.html.twig', [ 72 | 'clients' => $clients, 73 | 'stats' => $stats, 74 | ]); 75 | } 76 | 77 | #[Route('/api/clients/new', name: 'new_client')] 78 | public function createClient(Request $request, EntityManagerInterface $entityManager, ClientManagerInterface $clientManager): Response 79 | { 80 | $apiClient = new ApiClient(); 81 | 82 | $form = $this->createFormBuilder($apiClient) 83 | ->add('name', TextType::class) 84 | ->add('email', TextType::class) 85 | ->add('url', TextType::class) 86 | ->add('description', TextType::class) 87 | // Used to create the Oauth2 client 88 | ->add('active', CheckboxType::class, ['mapped' => false]) 89 | ->add('identifier', TextType::class, ['mapped' => false]) 90 | ->add('secret', TextType::class, ['mapped' => false]) 91 | ->getForm(); 92 | 93 | $form->handleRequest($request); 94 | 95 | if ($form->isSubmitted() && $form->isValid()) { 96 | $apiClient = $form->getData(); 97 | $apiClient->setCreatedAt(new \DateTime()); 98 | 99 | // OAuth2 client fields 100 | $active = $form->get('active')->getData(); 101 | $identifier = $form->get('identifier')->getData(); 102 | $secret = $form->get('secret')->getData(); 103 | 104 | $client = new Client($apiClient->getName(), $identifier, $secret); 105 | $client->setActive($active); 106 | 107 | $client 108 | ->setGrants(new Grant('client_credentials'), new Grant('refresh_token')) 109 | ->setScopes(new Scope('api')) 110 | ; 111 | 112 | $clientManager->save($client); 113 | 114 | $apiClient->setOauth2ClientIdentifier($identifier); 115 | 116 | $entityManager->persist($apiClient); 117 | $entityManager->flush(); 118 | 119 | return $this->redirectToRoute('admin_clients'); 120 | } 121 | 122 | return $this->render('admin/clients.new.html.twig', [ 123 | 'form' => $form, 124 | ]); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Controller/SecurityController.php: -------------------------------------------------------------------------------- 1 | getUser()) { 17 | return $this->redirectToRoute('admin_dashboard'); 18 | } 19 | 20 | // get the login error if there is one 21 | $error = $authenticationUtils->getLastAuthenticationError(); 22 | // last username entered by the user 23 | $lastUsername = $authenticationUtils->getLastUsername(); 24 | 25 | return $this->render('admin/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); 26 | } 27 | 28 | #[Route('/admin/logout', name: 'admin_logout')] 29 | public function logout() 30 | { 31 | throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Dataclass/MusicalEntity/MusicalEntityInterface.php: -------------------------------------------------------------------------------- 1 | identifier) 34 | #[ORM\Column(type: 'string', length: 80)] 35 | private $oauth2ClientIdentifier; 36 | 37 | /** 38 | * @return (Role|string)[] The user roles 39 | */ 40 | public function getRoles(): array 41 | { 42 | // Only used in stateful version, when we bypass the oauth 43 | return ['ROLE_BYPASS_AUTH_API']; 44 | } 45 | 46 | /** 47 | * Removes sensitive data from the user. 48 | * 49 | * This is important if, at any given point, sensitive information like 50 | * the plain-text password is stored on this object. 51 | */ 52 | public function eraseCredentials(): void 53 | { 54 | } 55 | 56 | public function getUserIdentifier(): string 57 | { 58 | return $this->name; 59 | } 60 | 61 | public function getName(): string 62 | { 63 | return $this->name; 64 | } 65 | 66 | public function setName(string $name): self 67 | { 68 | $this->name = $name; 69 | 70 | return $this; 71 | } 72 | 73 | public function getEmail(): string 74 | { 75 | return $this->email; 76 | } 77 | 78 | public function setEmail(string $email): self 79 | { 80 | $this->email = $email; 81 | 82 | return $this; 83 | } 84 | 85 | public function getUrl(): ?string 86 | { 87 | return $this->url; 88 | } 89 | 90 | public function setUrl(?string $url): self 91 | { 92 | $this->url = $url; 93 | 94 | return $this; 95 | } 96 | 97 | public function getOauth2ClientIdentifier(): string 98 | { 99 | return $this->oauth2ClientIdentifier; 100 | } 101 | 102 | public function setOauth2ClientIdentifier(string $oauth2ClientIdentifier): self 103 | { 104 | $this->oauth2ClientIdentifier = $oauth2ClientIdentifier; 105 | 106 | return $this; 107 | } 108 | 109 | public function getDescription(): ?string 110 | { 111 | return $this->description; 112 | } 113 | 114 | public function setDescription(?string $description): self 115 | { 116 | $this->description = $description; 117 | 118 | return $this; 119 | } 120 | 121 | public function getCreatedAt(): \DateTime 122 | { 123 | return $this->createdAt; 124 | } 125 | 126 | public function setCreatedAt(\DateTime $createdAt): self 127 | { 128 | $this->createdAt = clone $createdAt; 129 | 130 | return $this; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Entity/Item.php: -------------------------------------------------------------------------------- 1 | true])] 20 | private $id; 21 | 22 | #[ORM\Column(type: 'string', length: 170, nullable: true)] 23 | private $intent; 24 | 25 | #[ORM\Column(type: 'blob', nullable: false, length: 65532)] 26 | private $object; 27 | 28 | #[ORM\Column(type: 'string', length: 170, nullable: true)] 29 | private $track; 30 | 31 | #[ORM\Column(type: 'string', length: 170, nullable: true)] 32 | private $album; 33 | 34 | #[ORM\Column(type: 'string', length: 170, nullable: true)] 35 | private $artist; 36 | 37 | #[ORM\Column(type: 'datetime', nullable: false)] 38 | private $createdAt; 39 | 40 | #[ORM\Column(type: 'datetime', nullable: true)] 41 | private $expiresAt; 42 | 43 | #[ORM\Column(type: 'string', length: 170, nullable: false)] 44 | private $signature; 45 | 46 | #[ORM\Column(type: 'string', length: 80, nullable: true)] 47 | private $clientId; 48 | 49 | public function getId(): int 50 | { 51 | return $this->id; 52 | } 53 | 54 | public function setIntent(?string $intent): self 55 | { 56 | $this->intent = $intent; 57 | 58 | return $this; 59 | } 60 | 61 | public function getMusicalEntity(): MusicalEntity|false 62 | { 63 | // Migrating from the previous namespace 64 | // It works because the class names have the same length (with the namespace) 65 | $migratedObject = str_replace( 66 | ['tuneefy\MusicalEntity\Entities\AlbumEntity', 'tuneefy\MusicalEntity\Entities\TrackEntity'], 67 | ['App\Dataclass\MusicalEntity\Entities\Album', 'App\Dataclass\MusicalEntity\Entities\Track'], 68 | $this->getObject() 69 | ); 70 | 71 | $objectAsMusicalEntity = unserialize($migratedObject); 72 | 73 | return $objectAsMusicalEntity; 74 | } 75 | 76 | public function getObject(): string 77 | { 78 | if (is_resource($this->object)) { 79 | $this->object = stream_get_contents($this->object); 80 | } 81 | 82 | return $this->object; 83 | } 84 | 85 | public function setMusicalEntity(MusicalEntity $entity): self 86 | { 87 | $entityAsString = serialize($entity); 88 | $this->object = $entityAsString; 89 | 90 | return $this; 91 | } 92 | 93 | public function setObject(string $object): self 94 | { 95 | $this->object = $object; 96 | 97 | return $this; 98 | } 99 | 100 | public function setTrack(?string $track): self 101 | { 102 | $this->track = $track; 103 | 104 | return $this; 105 | } 106 | 107 | public function setArtist(string $artist): self 108 | { 109 | $this->artist = $artist; 110 | 111 | return $this; 112 | } 113 | 114 | public function setAlbum(string $album): self 115 | { 116 | $this->album = $album; 117 | 118 | return $this; 119 | } 120 | 121 | public function setCreatedAt(\DateTime $createdAt): self 122 | { 123 | $this->createdAt = $createdAt; 124 | 125 | return $this; 126 | } 127 | 128 | public function setExpiresAt(?\DateTime $expiresAt): self 129 | { 130 | $this->expiresAt = $expiresAt; 131 | 132 | return $this; 133 | } 134 | 135 | public function getSignature(): string 136 | { 137 | return $this->signature; 138 | } 139 | 140 | public function setSignature(string $signature): self 141 | { 142 | $this->signature = $signature; 143 | 144 | return $this; 145 | } 146 | 147 | public function setClientId(?string $clientId): self 148 | { 149 | $this->clientId = $clientId; 150 | 151 | return $this; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/EventSubscriber/ApiBypassSubscriber.php: -------------------------------------------------------------------------------- 1 | isMainRequest()) { 28 | // don't do anything if it's not the main request 29 | return; 30 | } 31 | 32 | $request = $event->getRequest(); 33 | 34 | if ($request->query->has('access_token')) { 35 | // We have a token in the query, pass it down in the headers 36 | $request->headers->set('Authorization', 'Bearer '.$request->query->get('access_token')); 37 | 38 | return; 39 | } 40 | 41 | if (!$request->hasSession()) { 42 | // don't do anything if no session 43 | return; 44 | } 45 | 46 | if (!str_starts_with($request->getRequestUri(), '/api/v2/')) { 47 | // Only api requests 48 | return; 49 | } 50 | 51 | if (!$request->isXmlHttpRequest()) { 52 | return; 53 | } 54 | 55 | $session = $request->getSession(); 56 | $shouldBypass = $session->get('shouldBypass', false); 57 | 58 | if (!$shouldBypass) { 59 | return; 60 | } 61 | 62 | if (!$this->bypassClientIdentifier) { 63 | error_log('Bypass client identifier is not set'); 64 | 65 | return; 66 | } 67 | 68 | // Construct the ClientCredentials grant server 69 | $this->server->enableGrantType($this->grant); 70 | 71 | // Request a new token (valid only 2 minutes) 72 | $this->generatedToken = $this->grant->getAccessTokenForClient(new \DateInterval('PT2M'), $this->bypassClientIdentifier); 73 | 74 | if (null === $this->generatedToken) { 75 | return; 76 | } 77 | 78 | // Add the token to the request 79 | $request->headers->set('Authorization', 'Bearer '.$this->generatedToken->__toString()); 80 | } 81 | 82 | public function onTerminate(TerminateEvent $event): void 83 | { 84 | if (!$this->generatedToken) { 85 | return; 86 | } 87 | 88 | $this->repository->revokeAccessToken($this->generatedToken->getIdentifier()); 89 | } 90 | 91 | public static function getSubscribedEvents(): array 92 | { 93 | return [ 94 | // Just after the session listener: 95 | // Symfony\Component\HttpKernel\EventListener\SessionListener 96 | // which has a priority of 128 97 | KernelEvents::REQUEST => ['onRequest', 100], 98 | KernelEvents::TERMINATE => 'onTerminate', 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/EventSubscriber/ApiStatsSubscriber.php: -------------------------------------------------------------------------------- 1 | isMainRequest()) { 22 | // don't do anything if it's not the main request 23 | return; 24 | } 25 | 26 | $request = $event->getRequest(); 27 | 28 | if (!str_starts_with($request->getRequestUri(), '/api/v2/')) { 29 | // Only api requests 30 | return; 31 | } 32 | 33 | $token = $this->security->getToken(); 34 | 35 | if (!$token) { 36 | return; 37 | } 38 | 39 | $method = $request->attributes->get('api_method'); 40 | 41 | $clientId = $token->getAttribute('oauth_client_id'); 42 | 43 | if ($method && $clientId) { 44 | $this->statsService->addApiCallingStat($method, $clientId); 45 | } 46 | } 47 | 48 | public static function getSubscribedEvents(): array 49 | { 50 | return [ 51 | KernelEvents::TERMINATE => 'onTerminate', 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/EventSubscriber/LocaleSubscriber.php: -------------------------------------------------------------------------------- 1 | [ 21 | ['setLocaleFromCookie'], 22 | ], 23 | ]; 24 | } 25 | 26 | public function setLocaleFromCookie(RequestEvent $event): void 27 | { 28 | if (!$event->isMainRequest()) { 29 | // don't do anything if it's not the main request 30 | return; 31 | } 32 | 33 | $locale = $event->getRequest()->cookies->get('tuneefyLocale'); 34 | if ($locale) { 35 | $this->localeSwitcher->setLocale($locale); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | getEntityManager()->getConnection(); 19 | 20 | $sql = ' 21 | SELECT apic.*, c.*, COUNT(`items`.`id`) AS `nb_items` 22 | FROM api_client apic 23 | LEFT JOIN oauth2_client c on apic.oauth2_client_identifier = c.identifier 24 | LEFT JOIN items ON items.client_id = c.identifier 25 | GROUP BY apic.id 26 | ORDER BY apic.created_at DESC 27 | '; 28 | 29 | $resultSet = $conn->executeQuery($sql); 30 | 31 | // returns an array of arrays (i.e. a raw data set) 32 | return $resultSet->fetchAllAssociative(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Security/AdminUser.php: -------------------------------------------------------------------------------- 1 | username = $username; 16 | $this->password = $password; 17 | } 18 | 19 | /** 20 | * @return (Role|string)[] The user roles 21 | */ 22 | public function getRoles(): array 23 | { 24 | return ['ROLE_ADMIN']; 25 | } 26 | 27 | /** 28 | * Returns the password used to authenticate the user. 29 | */ 30 | public function getPassword(): string 31 | { 32 | return $this->password; 33 | } 34 | 35 | /** 36 | * Returns the salt that was originally used to encode the password. 37 | * 38 | * This can return null if the password was not encoded using a salt. 39 | * 40 | * @return string|null The salt 41 | */ 42 | public function getSalt() 43 | { 44 | return null; 45 | } 46 | 47 | /** 48 | * Returns the username used to authenticate the user. 49 | * 50 | * @return string The username 51 | */ 52 | public function getUsername() 53 | { 54 | return $this->username; 55 | } 56 | 57 | public function getUserIdentifier(): string 58 | { 59 | return $this->username; 60 | } 61 | 62 | /** 63 | * Removes sensitive data from the user. 64 | * 65 | * This is important if, at any given point, sensitive information like 66 | * the plain-text password is stored on this object. 67 | */ 68 | public function eraseCredentials(): void 69 | { 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Security/AdminUserProvider.php: -------------------------------------------------------------------------------- 1 | urlGenerator = $urlGenerator; 33 | $this->csrfTokenManager = $csrfTokenManager; 34 | $this->adminLogin = $adminLogin; 35 | $this->adminPassword = $adminPassword; 36 | } 37 | 38 | protected function getLoginUrl(Request $request): string 39 | { 40 | return $this->urlGenerator->generate('admin_login'); 41 | } 42 | 43 | public function supports(Request $request): bool 44 | { 45 | return 'admin_login' === $request->attributes->get('_route') 46 | && $request->isMethod('POST'); 47 | } 48 | 49 | public function authenticate(Request $request): Passport 50 | { 51 | $credentials = [ 52 | 'username' => $request->request->get('_username'), 53 | 'password' => $request->request->get('_password'), 54 | 'csrf_token' => $request->request->get('_csrf_token'), 55 | ]; 56 | $request->getSession()->set( 57 | SecurityRequestAttributes::LAST_USERNAME, 58 | $credentials['username'] 59 | ); 60 | 61 | if ($credentials['username'] !== $this->adminLogin) { 62 | // fail authentication with a custom error 63 | throw new CustomUserMessageAuthenticationException('Username could not be found.'); 64 | } 65 | 66 | if ($credentials['password'] !== $this->adminPassword) { 67 | // fail authentication with a custom error 68 | throw new CustomUserMessageAuthenticationException('Invalid credentials.'); 69 | } 70 | 71 | return new SelfValidatingPassport( 72 | new UserBadge($this->adminLogin), 73 | [new CsrfTokenBadge('authenticate', $credentials['csrf_token'])] 74 | ); 75 | } 76 | 77 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response 78 | { 79 | if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { 80 | return new RedirectResponse($targetPath); 81 | } 82 | 83 | return new RedirectResponse($this->urlGenerator->generate('admin_dashboard')); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Serializer/ApiErrorSerializer.php: -------------------------------------------------------------------------------- 1 | [['GENERAL_ERROR' => $exception->getMessage()]], 14 | ]; 15 | } 16 | 17 | public function supportsNormalization($data, ?string $format = null, array $context = []): bool 18 | { 19 | return $data instanceof FlattenException; 20 | } 21 | 22 | public function getSupportedTypes(?string $format): array 23 | { 24 | return [ 25 | FlattenException::class => __CLASS__ === self::class, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Services/ClientCredentialsGrant.php: -------------------------------------------------------------------------------- 1 | clientRepository->getClientEntity($clientId); 28 | 29 | if (!$client) { 30 | error_log("No client with identifier {$clientId} was found"); 31 | 32 | return null; 33 | } 34 | 35 | return $this->issueAccessToken($accessTokenTTL, $client, $client->getIdentifier(), []); 36 | } 37 | 38 | public function respondToAccessTokenRequest( 39 | ServerRequestInterface $request, 40 | ResponseTypeInterface $responseType, 41 | \DateInterval $accessTokenTTL 42 | ): ResponseTypeInterface { 43 | list($clientId) = $this->getClientCredentials($request); 44 | 45 | $client = $this->getClientEntityOrFail($clientId, $request); 46 | 47 | if (!$client->isConfidential()) { 48 | $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); 49 | 50 | throw OAuthServerException::invalidClient($request); 51 | } 52 | 53 | // Validate request 54 | $this->validateClient($request); 55 | 56 | $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); 57 | 58 | // Finalize the requested scopes 59 | $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client); 60 | 61 | // Issue and persist access token 62 | // [CHANGED]: we use the client identifier as user identifier, instead of null 63 | $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $client->getIdentifier(), $finalizedScopes); 64 | 65 | // Send event to emitter 66 | $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); 67 | 68 | // Inject access token into response type 69 | $responseType->setAccessToken($accessToken); 70 | 71 | return $responseType; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Services/Platforms/Interfaces/GeneralPlatformInterface.php: -------------------------------------------------------------------------------- 1 | getName().' platform did not respond correctly'.($message ? ': '.$message : '').'.'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Services/Platforms/PlatformResult.php: -------------------------------------------------------------------------------- 1 | musical_entity = $musical_entity; 34 | $this->metadata = $metadata; 35 | $this->intent = uniqid(); // We create it now for later use 36 | $this->expires = null; 37 | } 38 | 39 | public function getMetadata(): array 40 | { 41 | return $this->metadata; 42 | } 43 | 44 | public function getMusicalEntity(): ?MusicalEntity 45 | { 46 | return $this->musical_entity; 47 | } 48 | 49 | public function getIntent(): string 50 | { 51 | return $this->intent; 52 | } 53 | 54 | public function setExpires(\DateTime $expires): PlatformResult 55 | { 56 | $this->expires = $expires; 57 | 58 | return $this; 59 | } 60 | 61 | public function toArray(): array 62 | { 63 | if (null === $this->musical_entity) { 64 | return [ 65 | 'metadata' => $this->metadata, 66 | ]; 67 | } else { 68 | return [ 69 | 'musical_entity' => $this->musical_entity->toArray(), 70 | 'metadata' => $this->metadata, 71 | 'share' => [ 72 | 'intent' => $this->intent, 73 | 'expires' => $this->expires ? $this->expires->format(\DateTime::ATOM) : null, 74 | ], 75 | ]; 76 | } 77 | } 78 | 79 | public function mergeWith(PlatformResult $that): PlatformResult 80 | { 81 | $thatMusicalEntity = $that->getMusicalEntity(); 82 | 83 | if (null !== $this->musical_entity && null !== $thatMusicalEntity) { 84 | // Merge musical entities 85 | if ($this->musical_entity instanceof Track) { 86 | $this->musical_entity = Track::merge($this->musical_entity, $thatMusicalEntity); 87 | } elseif ($this->musical_entity instanceof Album) { 88 | $this->musical_entity = Album::merge($this->musical_entity, $thatMusicalEntity); 89 | } 90 | 91 | // Merge score 92 | $thatMetadata = $that->getMetadata(); 93 | if (array_key_exists('score', $this->metadata) && array_key_exists('score', $thatMetadata)) { 94 | $this->metadata['score'] = floatval($this->metadata['score']) + floatval($thatMetadata['score']); 95 | } 96 | 97 | // Merge other metadata 98 | $this->metadata['externalIds'] = array_merge($this->metadata['externalIds'], $thatMetadata['externalIds']); 99 | 100 | if (array_key_exists('merges', $this->metadata)) { 101 | $this->metadata['merges'] = $this->metadata['merges'] + 1; 102 | } else { 103 | $this->metadata['merges'] = 1; 104 | } 105 | } 106 | 107 | return $this; 108 | } 109 | 110 | public function finalizeMerge(): PlatformResult 111 | { 112 | // Compute a final score 113 | if (array_key_exists('merges', $this->metadata) && array_key_exists('score', $this->metadata)) { 114 | // The more merges, the better the result must be 115 | $merge_quantifier_offset = floatval($this->metadata['merges']) / 2.25; // Completely heuristic number 116 | $this->metadata['score'] = $merge_quantifier_offset + floatval($this->metadata['score']) / (floatval($this->metadata['merges']) + 1); 117 | } elseif (array_key_exists('score', $this->metadata)) { 118 | // has not been merged, ever. Lower score 119 | $this->metadata['score'] = floatval($this->metadata['score']) / 2; 120 | } else { 121 | $this->metadata['score'] = 0.0; 122 | } 123 | 124 | $this->metadata['score'] = round($this->metadata['score'], 3); 125 | 126 | return $this; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Utils/ApiUtils.php: -------------------------------------------------------------------------------- 1 | query->get('format') ?? $request->getPreferredFormat(); 21 | 22 | if (!in_array($format, self::ACCEPTABLE_FORMATS)) { 23 | $format = 'json'; 24 | } 25 | 26 | $mimeType = $request->getMimeType($format); 27 | 28 | if ($apiMethod) { 29 | $request->attributes->set('api_method', $apiMethod); 30 | } 31 | 32 | return new Response($this->serializer->serialize($data, $format), $httpStatus, [ 33 | 'Content-type' => $mimeType, 34 | ]); 35 | } 36 | 37 | public function createGenericErrorResponse(Request $request, string $error_code, int $httpStatus = 400): Response 38 | { 39 | return $this->createFormattedResponse($request, [ 40 | 'errors' => [PlatformEngine::ERRORS[$error_code]], 41 | ], $httpStatus); 42 | } 43 | 44 | public function createUnhandledErrorResponse(Request $request, string $message, int $httpStatus = 400): Response 45 | { 46 | return $this->createFormattedResponse($request, [ 47 | 'errors' => [['GENERAL_ERROR' => $message]], 48 | ], $httpStatus); 49 | } 50 | 51 | public function createUpstreamErrorResponse(Request $request, array $errorPayload, int $httpStatus = 400): Response 52 | { 53 | return $this->createFormattedResponse($request, $errorPayload, $httpStatus); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Utils/Utils.php: -------------------------------------------------------------------------------- 1 | base), 10, 36); 17 | } 18 | 19 | public function fromUId(string $uid): int 20 | { 21 | return intval(base_convert($uid, 36, 10) / $this->base); 22 | } 23 | 24 | /* 25 | Sanitizes a string 26 | Removes any non Latin character by its equivalent in ASCII Latin 27 | */ 28 | public static function sanitize(string $string): string 29 | { 30 | $string = transliterator_transliterate('Any-Latin; Latin-ASCII', $string); 31 | 32 | return strtolower(preg_replace('/[^\w\-]+/u', '-', $string)); 33 | } 34 | 35 | /* 36 | Returns an ellipsed version of $text 37 | */ 38 | public static function ellipsis(string $text, int $max = 100, string $append = '…'): string 39 | { 40 | if (strlen($text) <= $max) { 41 | return $text; 42 | } 43 | 44 | // Cut the string 45 | $out = trim(substr($text, 0, $max)); 46 | 47 | if (false === strpos($text, ' ')) { 48 | // If it's a single word, just return with the suffix 49 | return $out.$append; 50 | } else { 51 | // Else, we replace the last word with the suffix 52 | return substr($out, 0, strrpos($out, ' ') + 1).$append; 53 | } 54 | } 55 | 56 | /* 57 | Removes a bom from a string 58 | */ 59 | public static function removeBOM(string $string): string 60 | { 61 | $bom = pack('H*', 'EFBBBF'); 62 | 63 | return preg_replace("/^$bom/", '', $string); 64 | } 65 | 66 | public static function flattenMetaXMLNodes(string $xml): string 67 | { 68 | return preg_replace('/(.*)<\/meta>/', '<$1>$2', $xml); 69 | } 70 | 71 | /* 72 | When a platform does not return a "score" in a search session, 73 | we create a fake one using this function 74 | */ 75 | public static function indexScore(int $index): float 76 | { 77 | return round(1 / ($index / 10 + 1), 2); 78 | } 79 | 80 | /* 81 | Flattens tokens (an array of strings) to return a single "alphanumeric" string 82 | */ 83 | public static function flatten(array $tokens): string 84 | { 85 | return preg_replace('/[^a-z0-9]+/', '', strtolower(implode('', $tokens))); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /templates/_widget.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 | 21 | 22 |
    23 |
    24 | {% if params.statistics.ga_tracker_id > 0 %} 25 | 36 | {% endif %} 37 | 38 | 39 | 48 | 49 | 50 | 60 | -------------------------------------------------------------------------------- /templates/about.html.twig: -------------------------------------------------------------------------------- 1 | {% set schemaType, page = "WebPage", "about" %} 2 | 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block content %} 6 |
    7 |

    {{ "pages.about.title_long"|trans|raw }}

    8 | 9 |
    10 |
    {{ "facts.info"|trans|raw }}
    11 |
    {{ "facts.friends"|trans|raw }}
    12 |
    13 | 14 |
    15 |
    {{ "facts.pertinence"|trans|raw }}
    16 |
    {{ "facts.minify"|trans({"%minified%": "javascript:(function(){tb=document.createElement('SCRIPT');a.type='text/javascript';a.src='" ~ asset("/build/js/widget.js") ~ "?x='+(Math.random());document.getElementsByTagName('head')[0].appendChild(a);})();"})|raw }}
    17 |
    18 | 19 |
    20 | 22 |
    23 | {{ "facts.free"|trans|raw }} 24 |
    25 |
    26 | 27 |
    28 | 29 |
    30 |

    {{ "os.title_long"|trans|raw }}

    31 |
    32 |

    {{ "os.paragraph_1_important"|trans|raw }}

    33 |
    {{ "os.paragraph_1"|trans|raw }}
    34 |
    {{ "os.paragraph_2_thanks"|trans|raw }}
    35 |
    36 |
    37 | 38 |
    39 | 40 |
    41 |

    {{ "pages.about.team"|trans|raw }}

    42 |
    {{ "facts.team_idea"|trans|raw }}
    43 |
    {{ "facts.team_design"|trans|raw }}
    44 |
    45 |
    46 | 47 |

    {{ "contact_us.title"|trans|raw }}

    48 |
    49 |
    50 | 51 | 52 | 53 | {{ "contact_us.send"|trans|raw }} 54 |
    55 |
    {{ "mail.sending"|trans|raw }}
    56 |
    {{ "mail.success"|trans|raw }}
    57 |
    {{ "mail.error"|trans|raw }}
    58 |
    59 |
    60 |
    61 | {% endblock %} 62 | 63 | {% block javascript %} 64 | {{ parent() }} 65 | 66 | 67 | 74 | {% endblock %} 75 | -------------------------------------------------------------------------------- /templates/admin/_base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Administration 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 | 27 | 28 |
    29 | {% block content %} 30 | {% endblock %} 31 |
    32 |
    33 | 34 | {% block javascript %} 35 | 39 | 40 | 41 | 42 | {% endblock %} 43 | 44 | -------------------------------------------------------------------------------- /templates/admin/clients.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'admin/_base.html.twig' %} 2 | {% set menu = "clients" %} 3 | 4 | {% block content %} 5 |

    API Clients

    6 | 7 |

    Actions

    8 |

    Create new client

    9 | 10 |

    List

    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for client in clients %} 24 | {% set revoked = client.active?"":" class='negative'" %} 25 | 26 | 27 | 28 | 32 | 33 | 34 | 41 | 42 | {% endfor %} 43 | 44 |
    Name SinceDescription KeyItemsCalls
    {{ client.name }}{{ client.created_at|date('d/m/y') }} 29 | {% if client.description %}{{ client.description|default('N/A')}}
    {% endif %} 30 | {% if client.url %}{{ client.url }}
    {% endif %} 31 | {{ client.email }}
    {{ client.identifier }}{{ client.nb_items|number_format(0,'.',' ') }} 35 | {% if stats[client.identifier] is defined %} 36 | {% for item in stats[client.identifier] %} 37 | {{ item['method'] }} → {{ item['count']|number_format(0,'.',' ') }}
    38 | {% endfor %} 39 | {% endif %} 40 |
    45 | {% endblock %} 46 | 47 | {% block javascript %} 48 | {{ parent() }} 49 | API Clients 6 | 7 |

    Create a new client

    8 | 9 | {% if not form.vars.valid %} 10 |
    11 | 12 |
    13 | Error 14 |
    15 | {{ form_errors(form) }} 16 |
    17 | {% endif %} 18 | 19 | {{ form_start(form, {'attr': {'class': 'ui form'}}) }} 20 |
    21 |
    22 | 23 | {{ form_widget(form.name, {'attr': {'placeholder': 'API User'}}) }} 24 |
    25 |
    26 |
    27 | {{ form_widget(form.active) }} 28 | 29 |
    30 |
    31 |
    32 |
    33 |
    34 | 35 | {{ form_widget(form.email, {'attr': {'placeholder': 'user@example.org'}}) }} 36 |
    37 |
    38 | 39 | {{ form_widget(form.url, {'attr': {'placeholder': 'http://site.com'}}) }} 40 |
    41 |
    42 |
    43 |
    44 | 45 |
    46 | {{ form_widget(form.identifier) }} 47 | 48 |
    49 |
    50 |
    51 | 52 |
    53 | {{ form_widget(form.secret) }} 54 | 55 |
    56 |
    57 |
    58 |
    59 | 60 | {{ form_widget(form.description) }} 61 |
    62 | Back 63 | 64 | {{ form_end(form) }} 65 | 66 | 67 | {% endblock %} 68 | 69 | {% block javascript %} 70 | {{ parent() }} 71 | 106 | {% endblock %} 107 | -------------------------------------------------------------------------------- /templates/admin/dashboard.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'admin/_base.html.twig' %} 2 | {% set menu = "dashboard" %} 3 | 4 | {% block content %} 5 |

    Dashboard

    6 | 7 |

    Tracks & Albums

    8 |
    9 |
    10 |
    11 | {{ itemsStats.total|number_format(0, '.', ' ') }} 12 |
    13 |
    14 | Items total 15 |
    16 |
    17 |
    18 |
    19 | {{ itemsStats.tracks|number_format(0, '.', ' ') }} 20 |
    21 |
    22 | Tracks 23 |
    24 |
    25 |
    26 |
    27 | {{ (itemsStats.total - itemsStats.tracks)|number_format(0, '.', ' ') }} 28 |
    29 |
    30 | Albums 31 |
    32 |
    33 |
    34 | 35 |

    API

    36 |
    37 |
    38 |
    {{ activeClients|length }}
    39 |
    Active Clients
    40 |
    41 | {% for key, stat in apiStats %} 42 |
    43 |
    {{ stat }}
    44 |
    /{{ key }}
    45 |
    46 | {% endfor %} 47 |
    48 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/login.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'admin/_base.html.twig' %} 2 | {% set menu = null %} 3 | 4 | {% block content %} 5 | {% if app.user %} 6 |
    7 | Already logged in, logout 8 |
    9 | {% else %} 10 |
    11 |
    12 |

    13 |
    14 | Log-in 15 |
    16 |

    17 | {% if error %} 18 |
    19 |
    20 | Error 21 |
    22 | {{ error.messageKey|trans(error.messageData, 'security') }} 23 |
    24 | {% endif %} 25 |
    26 |
    27 |
    28 |
    29 | 30 | 31 |
    32 |
    33 |
    34 |
    35 | 36 | 37 |
    38 |
    39 | 40 | 41 |
    42 | 43 |
    44 | 45 |
    46 |
    47 |
    48 | {% endif %} 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /templates/api/api.less: -------------------------------------------------------------------------------- 1 | .action dl.inner { 2 | margin-top: 1px; 3 | } -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block social %} 17 | {{ ("pages." ~ page ~ ".title")|trans }} — tuneefy 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% endblock %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% if embed is defined and embed == true %}{% endif %} 35 | 36 | 37 | 38 | {% if embed is not defined or not embed %} 39 |
    40 | 41 | {% endif %} 42 | 43 |
    44 | 45 |
    46 | 56 |
    57 | 58 |
    59 |
    60 |
    61 |

    tuneefy

    62 |

    {{ "tagline"|trans|raw }}

    63 |
    64 | 65 | {% block content %}{% endblock %} 66 | 67 |
    68 |
    69 |
    70 | 71 | 80 | 81 | {% block javascript %} 82 | 85 | {% if ga_tracker_id %} 86 | 97 | {% endif %} 98 | 99 | 100 | {% endblock %} 101 | 102 | 103 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error.html.twig: -------------------------------------------------------------------------------- 1 | {% set schemaType, page = "WebPage", "500" %} 2 | 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block content %} 6 |
    7 |

    {{ "error.500.title"|trans }}

    8 |

    {{ "error.500.description"|trans|raw }}

    9 | {{ "error.action"|trans }} 10 |
    11 | {% endblock %} -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error404.html.twig: -------------------------------------------------------------------------------- 1 | {% set schemaType, page = "WebPage", "404" %} 2 | 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block content %} 6 |
    7 |

    {{ "error.404.title"|trans }}

    8 |

    {{ "error.404.description"|trans|raw }}

    9 | {{ "error.action"|trans }} 10 |
    11 | {% endblock %} -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error503.html.twig: -------------------------------------------------------------------------------- 1 | {% set schemaType, page = "WebPage", "503" %} 2 | 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block content %} 6 |
    7 |

    {{ "error.503.title"|trans }}

    8 |

    {{ "error.503.description"|trans|raw }}

    9 | {{ "error.action.api"|trans }} 10 |
    11 | {% endblock %} -------------------------------------------------------------------------------- /templates/macros/tools.twig: -------------------------------------------------------------------------------- 1 | {% macro ellipsis(txt, len) -%} 2 | {{ txt|length > len ? txt|slice(0, len) ~ '…' : txt }} 3 | {%- endmacro %} -------------------------------------------------------------------------------- /templates/trends.html.twig: -------------------------------------------------------------------------------- 1 | {% set schemaType, page = "WebPage", "trends" %} 2 | 3 | {% extends 'base.html.twig' %} 4 | 5 | {% block content %} 6 |
    7 |

    {{ "pages.trends.title_long"|trans|raw }}

    8 | 9 |
    10 |

    {{ "stats.global"|trans|raw }}

    11 | 12 | {% for hit in stats.hits|filter(hit => hit.platform.tag is defined) %} 13 | 18 | 19 | {% endfor %} 20 | 21 | 22 |
    23 | 24 |
      25 | {% for hit in stats.hits|filter(hit => hit.platform.tag is defined) %} 26 |
    • {{ hit.platform.name }}{{ (hit.count / total * 100)|number_format(1) }}%
    • 27 | {% endfor %} 28 |
    29 |
    30 | 31 | {% if stats.artists|length > 0 %} 32 |
    33 |

    {{ "stats.most_viewed_artists"|trans({"%limit%": most_viewed_stats_limit})|raw }}

    34 |
      35 | {% set base = (1 / stats.artists[0].count) * 170 %} 36 | {% for data in stats.artists %} 37 |
    • {{ data.artist }} 38 |
      {{ "stats.views"|trans({"%val%": data.count})|raw }} 39 |
    • 40 | {% endfor %} 41 |
    42 |
    43 | {% endif %} 44 | 45 | {% if stats.tracks|length > 0 %} 46 |
    47 |

    {{ "stats.most_viewed_tracks"|trans({"%limit%": most_viewed_stats_limit})|raw }}

    48 |
      49 | {% for key, data in stats.tracks %} 50 | {% set fontSize = 2.3*(1-loop.index/5) + (loop.index/5)*0.6 %} 51 |
    • {{ "stats.views"|trans({"%val%": data.count})|raw }} 52 | {{ loop.index }}{{ data.track }}{{ data.artist }} 53 |
    • 54 | {% endfor %} 55 |
    56 |
    57 | {% endif %} 58 | 59 | {% if stats.albums|length > 0 %} 60 |
    61 |

    {{ "stats.most_viewed_albums"|trans({"%limit%": most_viewed_stats_limit})|raw }}

    62 |
      63 | {% for key, data in stats.albums %} 64 | {% set fontSize = 2.3*(1-loop.index/5) + (loop.index/5)*0.6 %} 65 |
    • {{ "stats.views"|trans({"%val%": data.count})|raw }} 66 | {{ loop.index }}{{ data.album }}{{ data.artist }} 67 |
    • 68 | {% endfor %} 69 |
    70 |
    71 | {% endif %} 72 |
    73 | {% endblock %} 74 | 75 | 76 | {% block javascript %} 77 | {{ parent() }} 78 | 79 | 80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /tests/Acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | actor: AcceptanceTester 2 | suite_namespace: Tests\Acceptance 3 | modules: 4 | enabled: 5 | - PhpBrowser: 6 | url: http://127.0.0.1:9999/ -------------------------------------------------------------------------------- /tests/Acceptance/BackendCest.php: -------------------------------------------------------------------------------- 1 | stopFollowingRedirects(); 13 | 14 | $I->amOnPage('/admin/dashboard'); 15 | $I->seeResponseCodeIsRedirection(); 16 | $I->haveHttpHeader('Location', '/admin/login'); 17 | 18 | $I->amOnPage('/admin/api/clients'); 19 | $I->seeResponseCodeIsRedirection(); 20 | $I->haveHttpHeader('Location', '/admin/login'); 21 | 22 | $I->amOnPage('/admin/api/clients/new'); 23 | $I->seeResponseCodeIsRedirection(); 24 | $I->haveHttpHeader('Location', '/admin/login'); 25 | 26 | $I->amOnPage('/admin/login'); 27 | $I->seeResponseCodeIs(HttpCode::OK); 28 | } 29 | 30 | public function testLogin(AcceptanceTester $I) 31 | { 32 | $I->amOnPage('/admin/login'); 33 | $I->seeResponseCodeIs(HttpCode::OK); 34 | 35 | $I->submitForm('#loginForm', [ 36 | '_username' => 'admin', 37 | '_password' => 'test', 38 | ], 'submitButton'); 39 | 40 | $I->seeResponseCodeIs(HttpCode::OK); 41 | 42 | $I->dontSee('Log-in'); 43 | $I->see('Tracks & Albums'); 44 | 45 | $I->amOnPage('/admin/api/clients'); 46 | $I->seeResponseCodeIs(HttpCode::OK); 47 | $I->see('Create new client'); 48 | 49 | $I->amOnPage('/admin/api/clients/new'); 50 | $I->seeResponseCodeIs(HttpCode::OK); 51 | $I->see('Create a new client'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Acceptance/FrontendCest.php: -------------------------------------------------------------------------------- 1 | amOnPage('/'); 13 | $I->seeResponseCodeIs(HttpCode::OK); 14 | $I->see('Partager de la musique. Facilement.'); 15 | } 16 | 17 | public function testAbout(AcceptanceTester $I) 18 | { 19 | $I->amOnPage('/about'); 20 | $I->seeResponseCodeIs(HttpCode::OK); 21 | $I->see("la Vie, tuneefy, l'Univers et le Reste"); 22 | } 23 | 24 | public function testTrends(AcceptanceTester $I) 25 | { 26 | $I->amOnPage('/trends'); 27 | $I->seeResponseCodeIs(HttpCode::OK); 28 | $I->see('Les sites de musique que vous utilisez'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Api.suite.yml: -------------------------------------------------------------------------------- 1 | actor: ApiTester 2 | suite_namespace: Tests\Api 3 | step_decorators: 4 | - \Codeception\Step\AsJson 5 | modules: 6 | enabled: 7 | - Asserts 8 | - REST: 9 | url: http://127.0.0.1:9999/api/v2/ 10 | depends: PhpBrowser -------------------------------------------------------------------------------- /tests/Support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | writeln($message); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Support/UnitTester.php: -------------------------------------------------------------------------------- 1 | assertEquals( 18 | $utils->toUid(1000), 19 | $result 20 | ); 21 | 22 | $this->assertEquals( 23 | $utils->fromUid($result), 24 | 1000 25 | ); 26 | } 27 | 28 | public function testSanitize() 29 | { 30 | $string = "Je suis une chaîne prête pour la sanitization n'est-ce pas ?"; 31 | 32 | $this->assertEquals( 33 | Utils::sanitize($string), 34 | 'je-suis-une-chaine-prete-pour-la-sanitization-n-est-ce-pas-' 35 | ); 36 | } 37 | 38 | public function testEllipsis() 39 | { 40 | $string = "Je suis une chaîne prête pour l'ellipse n'est-ce pas ?"; 41 | 42 | $this->assertEquals( 43 | Utils::ellipsis($string), 44 | "Je suis une chaîne prête pour l'ellipse n'est-ce pas ?" 45 | ); 46 | 47 | $this->assertEquals( 48 | Utils::ellipsis($string, 12, '...'), 49 | 'Je suis ...' 50 | ); 51 | 52 | $this->assertEquals( 53 | Utils::ellipsis($string, 14, '...'), 54 | 'Je suis une ...' 55 | ); 56 | } 57 | 58 | public function testFlattenMetaXMLNodes() 59 | { 60 | $xml = '2Chainz'; 61 | $this->assertEquals( 62 | Utils::flattenMetaXMLNodes($xml), 63 | '2Chainz' 64 | ); 65 | 66 | $xml = '2Chainz'; 67 | $this->assertEquals( 68 | Utils::flattenMetaXMLNodes($xml), 69 | '2Chainz' 70 | ); 71 | } 72 | 73 | public function testIndexScore() 74 | { 75 | $this->assertEquals( 76 | Utils::indexScore(0), 77 | 1 78 | ); 79 | 80 | $this->assertEquals( 81 | Utils::indexScore(10), 82 | 0.5 83 | ); 84 | 85 | $this->assertEquals( 86 | Utils::indexScore(100), 87 | 0.09 88 | ); 89 | } 90 | 91 | public function testFlatten() 92 | { 93 | $tokens = ['token1', 'token2', 'other Token']; 94 | 95 | $this->assertEquals( 96 | Utils::flatten($tokens), 97 | 'token1token2othertoken' 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Unit/YoutubeVideoTitleParsingTest.php: -------------------------------------------------------------------------------- 1 | 'ARTIST - TITLE [Official Video]', 15 | 'result' => 'TITLE', 16 | ], 17 | [ 18 | 'source' => 'ARTIST - TITLE (Official)', 19 | 'result' => 'TITLE', 20 | ], 21 | [ 22 | 'source' => 'ARTIST - TITLE (Officiel)', 23 | 'result' => 'TITLE', 24 | ], 25 | [ 26 | 'source' => 'ARTIST - TITLE (CLIP OFFICIEL)', 27 | 'result' => 'TITLE', 28 | ], 29 | [ 30 | 'source' => 'ARTIST - TITLE (CLIP)', 31 | 'result' => 'TITLE', 32 | ], 33 | [ 34 | 'source' => 'ARTIST - TITLE (video officielle)', 35 | 'result' => 'TITLE', 36 | ], 37 | [ 38 | 'source' => 'ARTIST - TITLE (vidéo officielle)', 39 | 'result' => 'TITLE', 40 | ], 41 | ]; 42 | 43 | public function testTitleParsing() 44 | { 45 | $platform = new YoutubePlatform('test', 'test'); 46 | 47 | foreach ($this->strings as $str) { 48 | $this->assertEquals( 49 | $str['result'], 50 | $platform->parseYoutubeMusicVideoTitle($str['source'])[0] 51 | ); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | --------------------------------------------------------------------------------