├── .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 |
53 |
54 | {%- for key, links in item.links -%}
55 | {%- if links|length > 0 and compact|default(true) -%}
56 |
57 | {%- else -%}
58 | {%- for i, link in links -%}
59 |
60 | {%- endfor -%}
61 | {%- endif -%}
62 | {%- endfor -%}
63 |
64 |
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 |
53 |
54 | {%- for key, links in item.links -%}
55 | {%- if links|length > 0 and compact|default(true) -%}
56 |
57 | {%- else -%}
58 | {%- for i, link in links -%}
59 |
60 | {%- endfor -%}
61 | {%- endif -%}
62 | {%- endfor -%}
63 |
64 |
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$1>', $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 |
16 |
17 |
{{ "search.result_found_widget"|trans|raw }}
18 |
19 |
20 |
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 |
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 |
8 | Create new client
9 |
10 |
11 |
12 |
13 |
14 | Name |
15 | Since |
16 | Description |
17 | Key |
18 | Items |
19 | Calls |
20 |
21 |
22 |
23 | {% for client in clients %}
24 | {% set revoked = client.active?"":" class='negative'" %}
25 |
26 | {{ client.name }} |
27 | {{ client.created_at|date('d/m/y') }} |
28 |
29 | {% if client.description %}{{ client.description|default('N/A')}} {% endif %}
30 | {% if client.url %}{{ client.url }} {% endif %}
31 | {{ client.email }} |
32 | {{ client.identifier }} |
33 | {{ client.nb_items|number_format(0,'.',' ') }} |
34 |
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 | |
41 |
42 | {% endfor %}
43 |
44 |
45 | {% endblock %}
46 |
47 | {% block javascript %}
48 | {{ parent() }}
49 | API Clients
6 |
7 |
8 |
9 | {% if not form.vars.valid %}
10 |
11 |
12 |
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 |
6 |
7 |
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 |
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 |
17 | {% if error %}
18 |
19 |
22 | {{ error.messageKey|trans(error.messageData, 'security') }}
23 |
24 | {% endif %}
25 |
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 |
57 |
58 |
59 |
60 |
61 |
 }})
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 |
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 |
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 |
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 |
14 | {{ hit.platform.tag }}
15 | {{ hit.platform.name }}
16 | {{ hit.platform.color }}
17 | | {{ (hit.count / total * 100)|number_format(1) }}% |
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 |
--------------------------------------------------------------------------------