├── .gitignore
├── README.md
├── gulpfile.js
├── images
├── 128x128.png
├── 48x48.png
├── pattern.png
└── scrobble-icon.png
├── package.json
├── source
├── images
│ ├── 128x128.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── like-icon.png
│ ├── pattern.png
│ └── scrobble-icon.png
├── js
│ ├── app
│ │ ├── app.js
│ │ ├── audio.js
│ │ ├── main.js
│ │ ├── scrobbler.js
│ │ ├── video.js
│ │ └── watcher.js
│ ├── libs
│ │ ├── lastapi.js
│ │ └── md5-min.js
│ ├── pages
│ │ ├── background.js
│ │ ├── lastauth.js
│ │ ├── popup.js
│ │ └── startup.js
│ └── utils
│ │ └── index.js
├── less
│ ├── lastauth.less
│ ├── popup.less
│ └── style.less
└── views
│ ├── lastauth.jade
│ └── popup.jade
└── vkobserver_2.3.5.crx
/.gitignore:
--------------------------------------------------------------------------------
1 | # osx noise
2 | .DS_Store
3 | profile
4 |
5 | # xcode noise
6 | build/*
7 | *.mode1
8 | *.mode1v3
9 | *.mode2v3
10 | *.perspective
11 | *.perspectivev3
12 | *.pbxuser
13 | *.xcworkspace
14 | xcuserdata
15 |
16 | # svn & cvs
17 | .svn
18 | CVS
19 |
20 | node_modules
21 | /css
22 | /images
23 | /js
24 | *.html
25 |
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VK.Observer
2 | Простое расширение для загрузки музыки из социальной сети Вконтакте (vk.com).
3 | Добавляет кнопку загрузки для каждой музыкальной записи в любом месте (стена, плеер и т.д.).
4 | Никакой рекламы, никаких трекеров и шпионов, открытый исходный код.
5 |
6 | Расширение было удалено из Chrome webstore ввиду жалоб ВК.
7 |
8 | Группа Вконтакте:
9 |
10 | https://vk.com/vkobserverchrome
11 |
12 | Неистово приветствуются предложения и code review.
13 |
14 | ```
15 | Я буду рад, если позаимствовав весь код или его часть, ты придешь к успеху не став мудаком.
16 | ```
17 |
18 | ### В текущей версии:
19 |
20 | * загрузка видео с именем файла как в интерфейсе Вконтакте
21 |
22 | * определение аудиозаписей, удаленных по просьбе правообладателя
23 |
24 | * загрузка аудиозаписей по клику на иконку загрузки в любом блоке и на любой странице
25 |
26 | * добавляет кнопку загрузки для каждой музыкальной записи в любом месте (стена, плеер и т.д.)
27 |
28 | * загрузка всех аудиозаписей для каждого поста по нажатию на кнопку "Загрузить все"
29 |
30 | * отображение размера файла и битрейта по наведению на аудиозапись
31 |
32 | * добавляет кнопки для загрузки видео с индикацией качества (только для видео, размещенного на серверах Вконтакте, не работает со сторонними видео-сервисами как YouTube и прочими)
33 |
34 | * поддерживается скробблинг и добавление в любимые треки для Last.fm
35 |
36 | ### В планах:
37 |
38 | * еще больше Last.fm
39 |
40 | ### Скриншоты:
41 | 
42 | * * *
43 | 
44 | * * *
45 | 
46 |
47 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const env = process.env.NODE_ENV,
4 | gulp = require('gulp'),
5 | gulpif = require('gulp-if'),
6 | cache = require('gulp-cache'),
7 | clean = require('gulp-rimraf'),
8 | stream = require('event-stream'),
9 | browserSync = require('browser-sync'),
10 | babel = require('gulp-babel'),
11 | browserify = require('browserify'),
12 | babelify = require('babelify'),
13 | uglify = require('gulp-uglify'),
14 | source = require('vinyl-source-stream'),
15 | sourcemaps = require('gulp-sourcemaps'),
16 | size = require('gulp-size'),
17 | jshint = require('gulp-jshint'),
18 | concat = require('gulp-concat'),
19 | minifyCSS = require('gulp-minify-css'),
20 | base64 = require('gulp-base64'),
21 | imagemin = require('gulp-imagemin'),
22 | less = require('gulp-less'),
23 | jade = require('gulp-jade'),
24 | rename = require('gulp-rename'),
25 | notify = require("gulp-notify"),
26 | pluginAutoprefix = require('less-plugin-autoprefix');
27 |
28 | const autoprefix = new pluginAutoprefix({ browsers: ["Chrome >= 30"] });
29 |
30 | gulp.task('html', () => {
31 | let localsObject = {};
32 |
33 | gulp.src('source/views/*.jade')
34 | .pipe(jade({
35 | locals: localsObject,
36 | pretty: true
37 | }))
38 | .pipe(gulp.dest(''))
39 | .pipe(browserSync.reload({stream:true}));
40 | });
41 |
42 | gulp.task('styles', () => {
43 | return gulp.src('source/less/*.less')
44 | .pipe(less({
45 | plugins: [autoprefix]
46 | }))
47 | .on("error", notify.onError({
48 | message: 'LESS compile error: <%= error.message %>'
49 | }))
50 | .pipe(base64({
51 | extensions: ['jpg', 'png', 'svg'],
52 | maxImageSize: 32*1024
53 | }))
54 | .pipe(minifyCSS({
55 | keepBreaks: false
56 | }))
57 | .pipe(gulp.dest('css'))
58 | .pipe(size({
59 | title: 'size of styles'
60 | }))
61 | .pipe(browserSync.reload({stream:true}));
62 | });
63 |
64 | gulp.task('scripts', () => {
65 | let modules = browserify('source/js/app/app.js', {
66 | debug: env === "development" ? true : false
67 | })
68 | .transform(babelify, {presets: ["es2015"]})
69 | .bundle()
70 | .on("error", notify.onError({
71 | message: 'Browserify error: <%= error.message %>'
72 | }))
73 | .pipe(source('vk-observer.js'))
74 | .pipe(gulp.dest('js'))
75 | .pipe(size({
76 | title: 'size of modules'
77 | }));
78 | let deps = gulp.src('source/js/libs/*')
79 | .pipe(concat('libs.js'))
80 | .pipe(uglify())
81 | .pipe(gulp.dest('js'))
82 | .pipe(size({
83 | title: 'size of js dependencies'
84 | }));
85 | let pages = gulp.src('source/js/pages/*.js')
86 | .pipe(gulpif(env === "development", sourcemaps.init()))
87 | .pipe(babel({
88 | presets: ['es2015']
89 | }))
90 | .on("error", notify.onError({
91 | message: 'Babel error: <%= error.message %>'
92 | }))
93 | .pipe(gulpif(env === "development", sourcemaps.write()))
94 | .pipe(gulp.dest('js'))
95 | .pipe(size({
96 | title: 'size of modules'
97 | }));
98 | stream.concat(modules, deps, pages);
99 | });
100 |
101 | gulp.task('images', () => {
102 | return gulp.src(['source/images/*', '!source/images/*.db'])
103 | .pipe(cache(imagemin({
104 | optimizationLevel: 5,
105 | progressive: true,
106 | interlaced: true
107 | })))
108 | .on("error", notify.onError({
109 | message: 'Images processing error: <%= error.message %>'
110 | }))
111 | .pipe(gulp.dest('images'))
112 | .pipe(size({
113 | title: 'size of images'
114 | }));
115 | });
116 |
117 | gulp.task('clean', () => {
118 | return gulp.src(['css', 'js', '*.html'], {read: false})
119 | .pipe(clean());
120 | });
121 |
122 | gulp.task('clear', (done) => {
123 | return cache.clearAll(done);
124 | });
125 |
126 | gulp.task('watch', () => {
127 | gulp.watch('source/views/**/*.jade', ['html']);
128 | gulp.watch('source/js/**/*.js', ['scripts']);
129 | gulp.watch('source/less/**/*.less', ['styles']);
130 | gulp.watch('source/images/*', ['images']);
131 | });
132 |
133 | gulp.task('default', ['clean', 'clear'], () => {
134 | gulp.start('styles', 'scripts', 'images', 'html');
135 | });
136 |
--------------------------------------------------------------------------------
/images/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/images/128x128.png
--------------------------------------------------------------------------------
/images/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/images/48x48.png
--------------------------------------------------------------------------------
/images/pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/images/pattern.png
--------------------------------------------------------------------------------
/images/scrobble-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/images/scrobble-icon.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vk-observer",
3 | "version": "2.3.1",
4 | "description": "VK music/video download",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/glebcha/vk-observer.git"
8 | },
9 | "author": "Glebcha",
10 | "license": "MIT",
11 | "homepage": "https://github.com/glebcha/vk-observer.git",
12 | "scripts": {
13 | "start": "NODE_ENV=production gulp && gulp watch",
14 | "start-dev": "NODE_ENV=development gulp && gulp watch"
15 | },
16 | "devDependencies": {
17 | "gulp-sourcemaps": "^1.12.0",
18 | "gulp-util": "^3.0.8",
19 | "babel": "^6.3.13",
20 | "babel-preset-es2015": "^6.24.1",
21 | "babelify": "^7.3.0",
22 | "browser-sync": "^2.18.12",
23 | "browserify": "^14.4.0",
24 | "event-stream": "^3.3.4",
25 | "gulp": "^3.9.1",
26 | "gulp-babel": "^6.1.2",
27 | "gulp-base64": "^0.1.3",
28 | "gulp-cache": "^0.4.6",
29 | "gulp-concat": "^2.6.1",
30 | "gulp-if": "^2.0.2",
31 | "gulp-imagemin": "^3.3.0",
32 | "gulp-jade": "^1.1.0",
33 | "gulp-jshint": "^2.0.4",
34 | "gulp-less": "^3.3.2",
35 | "gulp-minify-css": "^1.2.4",
36 | "gulp-notify": "^3.0.0",
37 | "gulp-rename": "^1.2.2",
38 | "gulp-rimraf": "^0.2.1",
39 | "gulp-size": "^2.1.0",
40 | "gulp-uglify": "^3.0.0",
41 | "jshint": "^2.9.5",
42 | "less-plugin-autoprefix": "^1.5.1",
43 | "vinyl-source-stream": "^1.1.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/source/images/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/source/images/128x128.png
--------------------------------------------------------------------------------
/source/images/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/source/images/32x32.png
--------------------------------------------------------------------------------
/source/images/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/source/images/48x48.png
--------------------------------------------------------------------------------
/source/images/like-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/source/images/like-icon.png
--------------------------------------------------------------------------------
/source/images/pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/source/images/pattern.png
--------------------------------------------------------------------------------
/source/images/scrobble-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/source/images/scrobble-icon.png
--------------------------------------------------------------------------------
/source/js/app/app.js:
--------------------------------------------------------------------------------
1 | import vkObserver from './main';
2 | import Watcher from './watcher';
3 |
4 | const vk = new vkObserver();
5 | const watcher = new Watcher();
6 |
7 | vk.syncStorage();
8 | watcher.observe();
9 |
--------------------------------------------------------------------------------
/source/js/app/audio.js:
--------------------------------------------------------------------------------
1 | import vkObserver from './main';
2 | import { xhr, decodeURL, getJSON } from '../utils';
3 |
4 | class Audio extends vkObserver {
5 | constructor() {
6 | super();
7 | this.getAllAudios = this.getAllAudios.bind(this);
8 | }
9 |
10 | getBlob(event) {
11 | const el = event.target;
12 | const wrap = el.parentNode;
13 | const url = el.href;
14 | const downloaded = event.target.getAttribute('data-enabled');
15 |
16 | if (!downloaded) {
17 | const statusBlock = document.createElement('span');
18 |
19 | event.target.setAttribute('data-enabled', true);
20 | event.preventDefault();
21 | event.stopPropagation();
22 |
23 | el.style.visibility = 'hidden';
24 | statusBlock.className = 'cached-status';
25 | wrap.appendChild(statusBlock);
26 |
27 | xhr({
28 | url,
29 | method: 'GET',
30 | responseType: 'blob',
31 | onProgress: (completion) => {
32 | const cachedCompletion = Math.floor(completion.loaded / completion.total * 100);
33 | const cachedPercent = cachedCompletion + '%';
34 |
35 | statusBlock.innerHTML = '';
36 | statusBlock.innerHTML = cachedPercent;
37 |
38 | if (cachedPercent === '100%') {
39 | statusBlock.remove();
40 | el.style.visibility = 'visible';
41 | }
42 | },
43 | }).then((response) => {
44 | const winUrl = window.URL || window.webkitURL;
45 | const blob = new window.Blob([response], {
46 | 'type': 'audio/mpeg'
47 | });
48 | const link = winUrl.createObjectURL(blob);
49 |
50 | el.href = link;
51 |
52 | el.click();
53 | el.removeEventListener('click', this.getBlob, false);
54 | // winUrl.revokeObjectURL(link);
55 | // el.href = getLink;
56 | });
57 | }
58 |
59 | }
60 |
61 | displayBitrate(target, options) {
62 | const { url, id, title, duration } = options;
63 | const bitrateStatus = localStorage.VkObserver_bitrate;
64 |
65 | xhr({
66 | url,
67 | method: 'GET',
68 | headers: [{
69 | name: 'Range',
70 | value: 'bytes=0-1'
71 | }],
72 | optional: {
73 | id,
74 | title,
75 | duration,
76 | calculateBitrate: true,
77 | }
78 | })
79 | .then(data => {
80 | const { fileSize, bitrate } = data.optional
81 |
82 | if (bitrateStatus === 'enabled' && !target.querySelector('.bitrate')) {
83 | let text;
84 | if (isNaN(bitrate.kbps) === true) {
85 | text = '×';
86 | } else {
87 | text = `${ bitrate.kbps } кбит/с${ fileSize } МБ`;
88 | }
89 | let b = document.createElement('span');
90 |
91 | b.className = 'bitrate';
92 | b.innerHTML = text.replace('-', '');
93 | target.appendChild(b);
94 | }
95 |
96 | })
97 |
98 | }
99 |
100 | setAudioUrl(target, options) {
101 | const { id, title, duration, userId, extensionId } = options;
102 | const isError = target.getAttribute('data-fetch-error');
103 | const isFetching = target.getAttribute('data-fetching');
104 | const downloadBtn = target.querySelector('.download-link');
105 | const audioInfo = target.querySelector('.audio_row__inner');
106 |
107 | if(isFetching) return;
108 |
109 | target.setAttribute('data-fetching', true);
110 |
111 | const form = new FormData();
112 |
113 | form.append('act', 'reload_audio');
114 | form.append('al', '1');
115 | form.append('ids', extensionId);
116 |
117 | return xhr({
118 | url: 'https://vk.com/al_audio.php',
119 | method: 'POST',
120 | body: form
121 | })
122 | .then(response => {
123 | const filteredUrls = response.result
124 | .split(',')
125 | .filter(item => item.indexOf('mp3') >= 0);
126 |
127 | const cleanUrl = filteredUrls[0].replace(/^"(.+(?="$))"$/, '$1');
128 |
129 | return decodeURL(cleanUrl, userId);
130 | })
131 | .then(url => {
132 | let error = false;
133 |
134 | target.removeAttribute('data-fetching');
135 | target.removeAttribute('data-fetch-error');
136 |
137 | if(url.indexOf('audio_api_unavailable') >= 0) {
138 | error = true;
139 | target.setAttribute('data-fetch-error', true);
140 | }
141 |
142 | if(!error && !downloadBtn) {
143 | const d = document.createElement('a');
144 |
145 | target.setAttribute('data-fetched', true);
146 |
147 | d.className = 'download-link';
148 | d.href = url;
149 | d.setAttribute('download', title);
150 | d.addEventListener('click', this.getBlob, false);
151 | audioInfo.insertBefore(d, audioInfo.firstChild);
152 |
153 | options.url = url;
154 | this.displayBitrate(target, options);
155 | }
156 |
157 | })
158 | .catch(err => {
159 | target.removeAttribute('data-fetching');
160 | target.setAttribute('data-fetch-error', true);
161 | // console.error('SET_AUDIO_URL', err, JSON.stringify(err))
162 | })
163 | }
164 |
165 | getAudioExtra(audioData) {
166 | const {
167 | content_id,
168 | duration,
169 | vk_id
170 | } = audioData.find(el => el && [].toString.call(el) === "[object Object]")
171 | const extensions = audioData.filter(el => el && [].toString.call(el) === "[object String]" && !el.match(/http?s/g) && el.match(/\/\//g))
172 | const extensionDatas = extensions.length &&
173 | extensions[0]
174 | .split('/')
175 | .map(item => item.replace('/', ''))
176 | .filter(item => item.length > 0)
177 | const extensionId = `${content_id}_${extensionDatas[2]}_${extensionDatas[extensionDatas.length - 1]}, ${content_id}`
178 |
179 | return {
180 | extensionId,
181 | content_id,
182 | duration,
183 | vk_id,
184 | }
185 | }
186 |
187 | getAudioBlockOptions(audioBlock) {
188 | const audioData = getJSON(audioBlock.getAttribute('data-audio'));
189 | const {
190 | extensionId,
191 | content_id,
192 | duration,
193 | vk_id,
194 | } = this.getAudioExtra(audioData)
195 | const audioTitle = audioBlock.querySelector('.audio_row__title_inner').innerText;
196 | const audioArtist = audioBlock.querySelector('.audio_row__performers').innerText;
197 | const audioName = audioArtist + "-" + audioTitle;
198 | const audioFullName = audioName.replace(/(<([^>]+)>)|([<>:"\/\\|?*.])/ig, '');
199 |
200 | return {
201 | id: content_id,
202 | title: audioFullName,
203 | userId: vk_id,
204 | duration,
205 | extensionId,
206 | };
207 | }
208 |
209 | showA(audios) {
210 | let audioBlocks = audios || document.querySelectorAll('.audio_row');
211 | audioBlocks = [].slice.call(audioBlocks);
212 |
213 | if (audioBlocks.length > 0) {
214 | audioBlocks.forEach(audioBlock => {
215 | const btn = audioBlock.querySelector('.audio_row_content');
216 |
217 | if (!btn.querySelector('.download-link')) {
218 | const self = this;
219 | const options = this.getAudioBlockOptions(audioBlock);
220 |
221 | audioBlock.addEventListener(
222 | 'mouseover',
223 | function handler(e) {
224 | const isClaimed = this.className.indexOf('claimed') >= 0;
225 | const isDeleted = this.className.indexOf('audio_deleted') >= 0;
226 | const isFetched = this.getAttribute('data-fetched');
227 |
228 | if(isFetched || isClaimed || isDeleted) {
229 | this.removeEventListener('mouseover', handler, false);
230 | }
231 |
232 | self.setAudioUrl(this, options);
233 | },
234 | false
235 | );
236 |
237 | }
238 | })
239 | }
240 |
241 | }
242 |
243 | getAllAudios(event) {
244 | event.preventDefault();
245 | const btn = event.target
246 | const item = btn.parentNode;
247 | const isFetching = btn.getAttribute('data-fetching');
248 | const notFetchedAudioRows = item.querySelectorAll('.audio_row:not([data-fetched]):not([data-fetching])')
249 |
250 | if (isFetching || notFetchedAudioRows.length === 0) return
251 |
252 | btn.setAttribute('data-fetching', true)
253 |
254 | Promise.all([].slice.call(notFetchedAudioRows).map(audioBlock => {
255 | const options = this.getAudioBlockOptions(audioBlock);
256 | return this.setAudioUrl(audioBlock, options)
257 | .then(() => {
258 | const downloadBtn = audioBlock.querySelector('.download-link')
259 | downloadBtn && downloadBtn.click()
260 | })
261 | }))
262 | .then(() => btn.removeAttribute('data-fetching'))
263 | .catch(() => btn.removeAttribute('data-fetching'))
264 | }
265 |
266 | getA(entries) {
267 | let posts = entries || document.querySelectorAll('.post');
268 | posts = [].slice.call(posts);
269 |
270 | posts.forEach((post) => {
271 | let wallText = post.querySelector('.wall_text');
272 |
273 | if(wallText === null) {
274 | wallText = post;
275 | }
276 |
277 | if (post !== undefined && post !== null) {
278 | if (wallText.querySelectorAll('.audio_row').length > 1) {
279 | const btn = document.createElement('a');
280 | const btnHTML = `Загрузить все
281 |
282 | Нажмите, чтобы загрузить все аудиозаписи
283 |
284 | `
285 |
286 | btn.href = '#';
287 | btn.className = 'download-all-link';
288 | btn.innerHTML = btnHTML;
289 | btn.addEventListener('click', this.getAllAudios, false);
290 |
291 | if (!post.querySelector('.download-all-link')) {
292 | wallText.appendChild(btn);
293 | }
294 |
295 | }
296 | }
297 | })
298 | }
299 | }
300 |
301 | export default Audio
302 |
--------------------------------------------------------------------------------
/source/js/app/main.js:
--------------------------------------------------------------------------------
1 | class vkObserver {
2 |
3 | constructor() {
4 | this.storageSettings = {
5 | 'settings': {
6 | "bitrate": 'enabled',
7 | "cache": 'enabled',
8 | "scrobble": 'disabled'
9 | }
10 | }
11 | }
12 |
13 | clearStorage() {
14 | chrome.storage.sync.clear();
15 | chrome.storage.sync.set(this.storageSettings);
16 | }
17 |
18 | syncStorage() {
19 | const storage = chrome.storage.sync;
20 | storage.get('settings', (data) => {
21 | const storVal = data.settings;
22 |
23 | if (storVal === undefined) {
24 | this.clearStorage();
25 | localStorage.VkObserver_cache = 'enabled';
26 | localStorage.VkObserver_bitrate = 'enabled';
27 | localStorage.VkObserver_scrobble = 'disabled';
28 | }
29 |
30 | localStorage.VkObserver_cache = storVal.cache === 'disabled'
31 | ?
32 | 'disabled'
33 | :
34 | 'enabled';
35 | localStorage.VkObserver_bitrate = storVal.bitrate === 'disabled'
36 | ?
37 | 'disabled'
38 | :
39 | 'enabled';
40 | localStorage.VkObserver_scrobble = storVal.scrobble === 'disabled'
41 | ?
42 | 'disabled'
43 | :
44 | 'enabled';
45 |
46 | });
47 | }
48 |
49 |
50 | findClosest(el, selector) {
51 | let matchesFn;
52 |
53 | ['matches','webkitMatchesSelector'].some((fn) => {
54 | if (typeof document.body[fn] === 'function') {
55 | matchesFn = fn;
56 | return true;
57 | }
58 | return false;
59 | })
60 |
61 | while (el!==null) {
62 | parent = el.parentElement;
63 | if (parent!==null && parent[matchesFn](selector)) {
64 | return parent;
65 | }
66 | el = parent;
67 | }
68 |
69 | return null;
70 | }
71 |
72 | }
73 |
74 | export default vkObserver
75 |
--------------------------------------------------------------------------------
/source/js/app/scrobbler.js:
--------------------------------------------------------------------------------
1 | class Scrobbler {
2 | constructor() {}
3 |
4 | scrobble(songArtist, songTitle, statusIcon) {
5 | let scrobbleStatus = localStorage.VkObserver_scrobble,
6 | storage = chrome.storage.sync;
7 |
8 | storage.get('lastkeys', (data) => {
9 | let apiKey = data.lastkeys.api,
10 | apiSecret = data.lastkeys.secret,
11 | ts = Math.floor(new Date().getTime()/1000),
12 | lastfm = new LastFM({
13 | apiKey: apiKey,
14 | apiSecret: apiSecret
15 | });
16 |
17 | storage.get('lastsession', (data) => {
18 | const sk = data.lastsession;
19 | let startScrobble = () => {
20 | lastfm.track.scrobble(
21 | {artist: songArtist, track: songTitle, timestamp: ts},
22 | {key: sk},
23 | {
24 | success: (data) => {
25 | statusIcon.className = 'scrobbled';
26 | statusIcon.setAttribute('title', 'заскроблено');
27 | //console.log("Заскробблен! " + songArtist + " " + songTitle);
28 | },
29 | error: (code, message) => {
30 | console.log("Ошибка: " + message + " код: " + code);
31 | }
32 | }
33 | );
34 | }
35 |
36 | if (
37 | scrobbleStatus === 'enabled' &&
38 | songArtist !== null &&
39 | songArtist !== undefined
40 | ) {
41 | startScrobble();
42 | }
43 | });
44 |
45 | });
46 |
47 | }
48 |
49 |
50 | likeSong(songArtist, songTitle, likeIcon) {
51 | let scrobbleStatus = localStorage.VkObserver_scrobble,
52 | storage = chrome.storage.sync;
53 |
54 | storage.get('lastkeys', (data) => {
55 | let apiKey = data.lastkeys.api,
56 | apiSecret = data.lastkeys.secret,
57 | ts = Math.floor(new Date().getTime()/1000),
58 | lastfm = new LastFM({
59 | apiKey: apiKey,
60 | apiSecret: apiSecret
61 | });
62 |
63 | storage.get('lastsession', (data) => {
64 | const sk = data.lastsession;
65 | let like = () => {
66 | lastfm.track.love(
67 | {artist: songArtist, track: songTitle},
68 | {key: sk},
69 | {
70 | success: (data) => {
71 | likeIcon.className = 'liked';
72 | likeIcon.setAttribute('title', 'добавлено в любимые');
73 | //console.log("Добавлен в любимые! " + songArtist + " " + songTitle);
74 | },
75 | error: (code, message) => {
76 | console.log("Ошибка: " + message + " код: " + code);
77 | }
78 | }
79 | );
80 | },
81 | unlike = () => {
82 | lastfm.track.unlove(
83 | {artist: songArtist, track: songTitle},
84 | {key: sk},
85 | {
86 | success: (data) => {
87 | likeIcon.className = 'unliked';
88 | likeIcon.setAttribute('title', 'удалено из любимых');
89 | //console.log("Удален из любимых! " + songArtist + " " + songTitle);
90 | },
91 | error: (code, message) => {
92 | console.log("Ошибка: " + message + " код: " + code);
93 | }
94 | }
95 | );
96 | };
97 |
98 | if (
99 | scrobbleStatus === 'enabled' &&
100 | songArtist !== null &&
101 | songArtist !== undefined &&
102 | likeIcon.className !== 'changed'
103 | ) {
104 | if(
105 | likeIcon.className !== 'liked' ||
106 | likeIcon.className === 'unliked'
107 | ) {
108 | like();
109 | } else {
110 | unlike();
111 | }
112 | }
113 |
114 | });
115 |
116 | });
117 |
118 | }
119 | }
120 |
121 | export default Scrobbler
122 |
--------------------------------------------------------------------------------
/source/js/app/video.js:
--------------------------------------------------------------------------------
1 | import { xhr, defineVideoQuality } from '../utils';
2 |
3 | class Video {
4 | constructor() {
5 | this.currentQuality = 0;
6 | }
7 |
8 | showV(main, box) {
9 | let videoWrap = document.querySelector('#mv_layer_wrap');
10 | let parent = main || videoWrap;
11 |
12 | if (parent) {
13 | const videoBox = box || videoWrap.querySelector('#mv_box');
14 |
15 | if (videoBox) {
16 | const html5 = videoBox.querySelector('video');
17 | const sourceString = html5 && html5.getAttribute('src');
18 |
19 | if (sourceString) {
20 | const videoSrc = sourceString
21 | &&
22 | sourceString.split('?').slice(0, 1).toString();
23 |
24 | const isBlob = new RegExp('blob', 'g').test(videoSrc);
25 | const qualityItem = parent.querySelector('.videoplayer_quality_select_label_text');
26 | const qualityValue = qualityItem && parseInt(qualityItem.innerHTML);
27 | const quality = qualityValue && defineVideoQuality(qualityValue);
28 | const sideBar = parent.querySelector('.mv_actions_block>.clear_fix');
29 | const downloadBtn = sideBar && sideBar.querySelector('.video_btn');
30 |
31 | let videoTitle = parent.querySelector('.mv_min_title').innerText;
32 | videoTitle = /^\s*$/.test(videoTitle) ? 'VK-Video' : videoTitle;
33 |
34 | if (!isBlob && sideBar && !downloadBtn) {
35 | const btn = document.createElement('a');
36 |
37 | btn.href = videoSrc;
38 | btn.innerHTML = `${quality}`;
39 | btn.setAttribute('download', videoTitle);
40 | btn.className = 'like_btn video_btn';
41 | btn.addEventListener('click', function(event) {
42 | const {href, download} = this;
43 |
44 | event.preventDefault();
45 | chrome.runtime.sendMessage({
46 | id: 'video',
47 | videoSrc: href,
48 | videoTitle: download.replace(/(<([^>]+)>)|([<>:"\/\\|?*.])/ig, '')
49 | });
50 | });
51 | sideBar.appendChild(btn);
52 | this.currentQuality = qualityValue;
53 | }
54 | else if(!isBlob && sideBar && downloadBtn && qualityValue !== this.currentQuality) {
55 | downloadBtn.href = videoSrc;
56 | downloadBtn.innerHTML = quality;
57 | downloadBtn.setAttribute('download', videoTitle);
58 | this.currentQuality = qualityValue;
59 | }
60 |
61 | }
62 |
63 | }
64 | }
65 | }
66 | }
67 |
68 | export default Video
69 |
--------------------------------------------------------------------------------
/source/js/app/watcher.js:
--------------------------------------------------------------------------------
1 | import Audio from './audio';
2 | import Video from './video';
3 | import Scrobbler from './scrobbler';
4 |
5 | const audio = new Audio();
6 | const video = new Video();
7 | const scrobbler = new Scrobbler();
8 |
9 | class mediaWatcher {
10 |
11 | observe() {
12 | const body = document.body;
13 | const bodyConfig = {
14 | childList: true,
15 | subtree: true
16 | };
17 | let selectedQuality = '';
18 | let checker;
19 |
20 | const bodyObserver = new window.WebKitMutationObserver(
21 |
22 | (mutations) => {
23 | mutations.forEach( (mutation) => {
24 | const node = mutation.target;
25 | const audios = node.querySelectorAll('.audio_row');
26 | const blocks = node.querySelectorAll('.post');
27 | const videoModal = document.querySelector('.video_box_wrap');
28 | const isVideoModal = node.className === 'video_box_wrap';
29 | const isVideoQuality = node.className === 'videoplayer_quality_select_label_text';
30 | const canChangeQuality = isVideoModal || isVideoQuality;
31 |
32 | if (videoModal && canChangeQuality) {
33 | video.showV();
34 | }
35 |
36 | if (audios.length > 0) {
37 | audio.showA(audios);
38 | }
39 |
40 | if (blocks.length > 0) {
41 | audio.getA(blocks);
42 | }
43 |
44 | });
45 | }
46 |
47 | );
48 |
49 | bodyObserver.observe(body, bodyConfig);
50 | }
51 |
52 | }
53 |
54 | export default mediaWatcher;
55 |
--------------------------------------------------------------------------------
/source/js/libs/lastapi.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * Copyright (c) 2008-2010, Felix Bruns
4 | *
5 | */
6 |
7 | function LastFM(options){
8 | /* Set default values for required options. */
9 | var apiKey = options.apiKey || '';
10 | var apiSecret = options.apiSecret || '';
11 | var apiUrl = options.apiUrl || 'https://ws.audioscrobbler.com/2.0/';
12 | var cache = options.cache || undefined;
13 |
14 | /* Set API key. */
15 | this.setApiKey = function(_apiKey){
16 | apiKey = _apiKey;
17 | };
18 |
19 | /* Set API key. */
20 | this.setApiSecret = function(_apiSecret){
21 | apiSecret = _apiSecret;
22 | };
23 |
24 | /* Set API URL. */
25 | this.setApiUrl = function(_apiUrl){
26 | apiUrl = _apiUrl;
27 | };
28 |
29 | /* Set cache. */
30 | this.setCache = function(_cache){
31 | cache = _cache;
32 | };
33 |
34 | /* Set the JSONP callback identifier counter. This is used to ensure the callbacks are unique */
35 | var jsonpCounter = 0;
36 |
37 | /* Internal call (POST, GET). */
38 | var internalCall = function(params, callbacks, requestMethod){
39 | /* Cross-domain POST request (doesn't return any data, always successful). */
40 | if(requestMethod == 'POST'){
41 | /* Create iframe element to post data. */
42 | var html = document.getElementsByTagName('html')[0];
43 | var iframe = document.createElement('iframe');
44 | var doc;
45 |
46 | /* Set iframe attributes. */
47 | iframe.width = 1;
48 | iframe.height = 1;
49 | iframe.style.border = 'none';
50 | iframe.onload = function(){
51 | /* Remove iframe element. */
52 | //html.removeChild(iframe);
53 |
54 | /* Call user callback. */
55 | if(typeof(callbacks.success) != 'undefined'){
56 | callbacks.success();
57 | }
58 | };
59 |
60 | /* Append iframe. */
61 | html.appendChild(iframe);
62 |
63 | /* Get iframe document. */
64 | if(typeof(iframe.contentWindow) != 'undefined'){
65 | doc = iframe.contentWindow.document;
66 | }
67 | else if(typeof(iframe.contentDocument.document) != 'undefined'){
68 | doc = iframe.contentDocument.document.document;
69 | }
70 | else{
71 | doc = iframe.contentDocument.document;
72 | }
73 |
74 | /* Open iframe document and write a form. */
75 | doc.open();
76 | doc.clear();
77 | doc.write('');
87 | doc.write('');
90 |
91 | /* Close iframe document. */
92 | doc.close();
93 | }
94 | /* Cross-domain GET request (JSONP). */
95 | else{
96 | /* Get JSONP callback name. */
97 | var jsonp = 'jsonp' + new Date().getTime() + jsonpCounter;
98 |
99 | /* Update the unique JSONP callback counter */
100 | jsonpCounter += 1;
101 |
102 | /* Calculate cache hash. */
103 | var hash = auth.getApiSignature(params);
104 |
105 | /* Check cache. */
106 | if(typeof(cache) != 'undefined' && cache.contains(hash) && !cache.isExpired(hash)){
107 | if(typeof(callbacks.success) != 'undefined'){
108 | callbacks.success(cache.load(hash));
109 | }
110 |
111 | return;
112 | }
113 |
114 | /* Set callback name and response format. */
115 | params.callback = jsonp;
116 | params.format = 'json';
117 |
118 | /* Create JSONP callback function. */
119 | window[jsonp] = function(data){
120 | /* Is a cache available?. */
121 | if(typeof(cache) != 'undefined'){
122 | var expiration = cache.getExpirationTime(params);
123 |
124 | if(expiration > 0){
125 | cache.store(hash, data, expiration);
126 | }
127 | }
128 |
129 | /* Call user callback. */
130 | if(typeof(data.error) != 'undefined'){
131 | if(typeof(callbacks.error) != 'undefined'){
132 | callbacks.error(data.error, data.message);
133 | }
134 | }
135 | else if(typeof(callbacks.success) != 'undefined'){
136 | callbacks.success(data);
137 | }
138 |
139 | /* Garbage collect. */
140 | window[jsonp] = undefined;
141 |
142 | try{
143 | delete window[jsonp];
144 | }
145 | catch(e){
146 | /* Nothing. */
147 | }
148 |
149 | /* Remove script element. */
150 | if(head){
151 | head.removeChild(script);
152 | }
153 | };
154 |
155 | /* Create script element to load JSON data. */
156 | var head = document.getElementsByTagName("head")[0];
157 | var script = document.createElement("script");
158 |
159 | /* Build parameter string. */
160 | var array = [];
161 |
162 | for(var param in params){
163 | array.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
164 | }
165 |
166 | /* Set script source. */
167 | script.src = apiUrl + '?' + array.join('&').replace(/%20/g, '+');
168 |
169 | /* Append script element. */
170 | head.appendChild(script);
171 | }
172 | };
173 |
174 | /* Normal method call. */
175 | var call = function(method, params, callbacks, requestMethod){
176 | /* Set default values. */
177 | params = params || {};
178 | callbacks = callbacks || {};
179 | requestMethod = requestMethod || 'GET';
180 |
181 | /* Add parameters. */
182 | params.method = method;
183 | params.api_key = apiKey;
184 |
185 | /* Call method. */
186 | internalCall(params, callbacks, requestMethod);
187 | };
188 |
189 | /* Signed method call. */
190 | var signedCall = function(method, params, session, callbacks, requestMethod){
191 | /* Set default values. */
192 | params = params || {};
193 | callbacks = callbacks || {};
194 | requestMethod = requestMethod || 'GET';
195 |
196 | /* Add parameters. */
197 | params.method = method;
198 | params.api_key = apiKey;
199 |
200 | /* Add session key. */
201 | if(session && typeof(session.key) != 'undefined'){
202 | params.sk = session.key;
203 | }
204 |
205 | /* Get API signature. */
206 | params.api_sig = auth.getApiSignature(params);
207 |
208 | /* Call method. */
209 | internalCall(params, callbacks, requestMethod);
210 | };
211 |
212 | /* Album methods. */
213 | this.album = {
214 | addTags : function(params, session, callbacks){
215 | /* Build comma separated tags string. */
216 | if(typeof(params.tags) == 'object'){
217 | params.tags = params.tags.join(',');
218 | }
219 |
220 | signedCall('album.addTags', params, session, callbacks, 'POST');
221 | },
222 |
223 | getBuylinks : function(params, callbacks){
224 | call('album.getBuylinks', params, callbacks);
225 | },
226 |
227 | getInfo : function(params, callbacks){
228 | call('album.getInfo', params, callbacks);
229 | },
230 |
231 | getTags : function(params, session, callbacks){
232 | signedCall('album.getTags', params, session, callbacks);
233 | },
234 |
235 | removeTag : function(params, session, callbacks){
236 | signedCall('album.removeTag', params, session, callbacks, 'POST');
237 | },
238 |
239 | search : function(params, callbacks){
240 | call('album.search', params, callbacks);
241 | },
242 |
243 | share : function(params, session, callbacks){
244 | /* Build comma separated recipients string. */
245 | if(typeof(params.recipient) == 'object'){
246 | params.recipient = params.recipient.join(',');
247 | }
248 |
249 | signedCall('album.share', params, callbacks);
250 | }
251 | };
252 |
253 | /* Artist methods. */
254 | this.artist = {
255 | addTags : function(params, session, callbacks){
256 | /* Build comma separated tags string. */
257 | if(typeof(params.tags) == 'object'){
258 | params.tags = params.tags.join(',');
259 | }
260 |
261 | signedCall('artist.addTags', params, session, callbacks, 'POST');
262 | },
263 |
264 | getCorrection : function(params, callbacks){
265 | call('artist.getCorrection', params, callbacks);
266 | },
267 |
268 | getEvents : function(params, callbacks){
269 | call('artist.getEvents', params, callbacks);
270 | },
271 |
272 | getImages : function(params, callbacks){
273 | call('artist.getImages', params, callbacks);
274 | },
275 |
276 | getInfo : function(params, callbacks){
277 | call('artist.getInfo', params, callbacks);
278 | },
279 |
280 | getPastEvents : function(params, callbacks){
281 | call('artist.getPastEvents', params, callbacks);
282 | },
283 |
284 | getPodcast : function(params, callbacks){
285 | call('artist.getPodcast', params, callbacks);
286 | },
287 |
288 | getShouts : function(params, callbacks){
289 | call('artist.getShouts', params, callbacks);
290 | },
291 |
292 | getSimilar : function(params, callbacks){
293 | call('artist.getSimilar', params, callbacks);
294 | },
295 |
296 | getTags : function(params, session, callbacks){
297 | signedCall('artist.getTags', params, session, callbacks);
298 | },
299 |
300 | getTopAlbums : function(params, callbacks){
301 | call('artist.getTopAlbums', params, callbacks);
302 | },
303 |
304 | getTopFans : function(params, callbacks){
305 | call('artist.getTopFans', params, callbacks);
306 | },
307 |
308 | getTopTags : function(params, callbacks){
309 | call('artist.getTopTags', params, callbacks);
310 | },
311 |
312 | getTopTracks : function(params, callbacks){
313 | call('artist.getTopTracks', params, callbacks);
314 | },
315 |
316 | removeTag : function(params, session, callbacks){
317 | signedCall('artist.removeTag', params, session, callbacks, 'POST');
318 | },
319 |
320 | search : function(params, callbacks){
321 | call('artist.search', params, callbacks);
322 | },
323 |
324 | share : function(params, session, callbacks){
325 | /* Build comma separated recipients string. */
326 | if(typeof(params.recipient) == 'object'){
327 | params.recipient = params.recipient.join(',');
328 | }
329 |
330 | signedCall('artist.share', params, session, callbacks, 'POST');
331 | },
332 |
333 | shout : function(params, session, callbacks){
334 | signedCall('artist.shout', params, session, callbacks, 'POST');
335 | }
336 | };
337 |
338 | /* Auth methods. */
339 | this.auth = {
340 | getMobileSession : function(params, callbacks){
341 | /* Set new params object with authToken. */
342 | params = {
343 | username : params.username,
344 | authToken : md5(params.username + md5(params.password))
345 | };
346 |
347 | signedCall('auth.getMobileSession', params, null, callbacks);
348 | },
349 |
350 | getSession : function(params, callbacks){
351 | signedCall('auth.getSession', params, null, callbacks);
352 | },
353 |
354 | getToken : function(callbacks){
355 | signedCall('auth.getToken', null, null, callbacks);
356 | },
357 |
358 | /* Deprecated. Security hole was fixed. */
359 | getWebSession : function(callbacks){
360 | /* Save API URL and set new one (needs to be done due to a cookie!). */
361 | var previuousApiUrl = apiUrl;
362 |
363 | apiUrl = 'http://ext.last.fm/2.0/';
364 |
365 | signedCall('auth.getWebSession', null, null, callbacks);
366 |
367 | /* Restore API URL. */
368 | apiUrl = previuousApiUrl;
369 | }
370 | };
371 |
372 | /* Chart methods. */
373 | this.chart = {
374 | getHypedArtists : function(params, session, callbacks){
375 | call('chart.getHypedArtists', params, callbacks);
376 | },
377 |
378 | getHypedTracks : function(params, session, callbacks){
379 | call('chart.getHypedTracks', params, callbacks);
380 | },
381 |
382 | getLovedTracks : function(params, session, callbacks){
383 | call('chart.getLovedTracks', params, callbacks);
384 | },
385 |
386 | getTopArtists : function(params, session, callbacks){
387 | call('chart.getTopArtists', params, callbacks);
388 | },
389 |
390 | getTopTags : function(params, session, callbacks){
391 | call('chart.getTopTags', params, callbacks);
392 | },
393 |
394 | getTopTracks : function(params, session, callbacks){
395 | call('chart.getTopTracks', params, callbacks);
396 | }
397 | };
398 |
399 | /* Event methods. */
400 | this.event = {
401 | attend : function(params, session, callbacks){
402 | signedCall('event.attend', params, session, callbacks, 'POST');
403 | },
404 |
405 | getAttendees : function(params, session, callbacks){
406 | call('event.getAttendees', params, callbacks);
407 | },
408 |
409 | getInfo : function(params, callbacks){
410 | call('event.getInfo', params, callbacks);
411 | },
412 |
413 | getShouts : function(params, callbacks){
414 | call('event.getShouts', params, callbacks);
415 | },
416 |
417 | share : function(params, session, callbacks){
418 | /* Build comma separated recipients string. */
419 | if(typeof(params.recipient) == 'object'){
420 | params.recipient = params.recipient.join(',');
421 | }
422 |
423 | signedCall('event.share', params, session, callbacks, 'POST');
424 | },
425 |
426 | shout : function(params, session, callbacks){
427 | signedCall('event.shout', params, session, callbacks, 'POST');
428 | }
429 | };
430 |
431 | /* Geo methods. */
432 | this.geo = {
433 | getEvents : function(params, callbacks){
434 | call('geo.getEvents', params, callbacks);
435 | },
436 |
437 | getMetroArtistChart : function(params, callbacks){
438 | call('geo.getMetroArtistChart', params, callbacks);
439 | },
440 |
441 | getMetroHypeArtistChart : function(params, callbacks){
442 | call('geo.getMetroHypeArtistChart', params, callbacks);
443 | },
444 |
445 | getMetroHypeTrackChart : function(params, callbacks){
446 | call('geo.getMetroHypeTrackChart', params, callbacks);
447 | },
448 |
449 | getMetroTrackChart : function(params, callbacks){
450 | call('geo.getMetroTrackChart', params, callbacks);
451 | },
452 |
453 | getMetroUniqueArtistChart : function(params, callbacks){
454 | call('geo.getMetroUniqueArtistChart', params, callbacks);
455 | },
456 |
457 | getMetroUniqueTrackChart : function(params, callbacks){
458 | call('geo.getMetroUniqueTrackChart', params, callbacks);
459 | },
460 |
461 | getMetroWeeklyChartlist : function(params, callbacks){
462 | call('geo.getMetroWeeklyChartlist', params, callbacks);
463 | },
464 |
465 | getMetros : function(params, callbacks){
466 | call('geo.getMetros', params, callbacks);
467 | },
468 |
469 | getTopArtists : function(params, callbacks){
470 | call('geo.getTopArtists', params, callbacks);
471 | },
472 |
473 | getTopTracks : function(params, callbacks){
474 | call('geo.getTopTracks', params, callbacks);
475 | }
476 | };
477 |
478 | /* Group methods. */
479 | this.group = {
480 | getHype : function(params, callbacks){
481 | call('group.getHype', params, callbacks);
482 | },
483 |
484 | getMembers : function(params, callbacks){
485 | call('group.getMembers', params, callbacks);
486 | },
487 |
488 | getWeeklyAlbumChart : function(params, callbacks){
489 | call('group.getWeeklyAlbumChart', params, callbacks);
490 | },
491 |
492 | getWeeklyArtistChart : function(params, callbacks){
493 | call('group.getWeeklyArtistChart', params, callbacks);
494 | },
495 |
496 | getWeeklyChartList : function(params, callbacks){
497 | call('group.getWeeklyChartList', params, callbacks);
498 | },
499 |
500 | getWeeklyTrackChart : function(params, callbacks){
501 | call('group.getWeeklyTrackChart', params, callbacks);
502 | }
503 | };
504 |
505 | /* Library methods. */
506 | this.library = {
507 | addAlbum : function(params, session, callbacks){
508 | signedCall('library.addAlbum', params, session, callbacks, 'POST');
509 | },
510 |
511 | addArtist : function(params, session, callbacks){
512 | signedCall('library.addArtist', params, session, callbacks, 'POST');
513 | },
514 |
515 | addTrack : function(params, session, callbacks){
516 | signedCall('library.addTrack', params, session, callbacks, 'POST');
517 | },
518 |
519 | getAlbums : function(params, callbacks){
520 | call('library.getAlbums', params, callbacks);
521 | },
522 |
523 | getArtists : function(params, callbacks){
524 | call('library.getArtists', params, callbacks);
525 | },
526 |
527 | getTracks : function(params, callbacks){
528 | call('library.getTracks', params, callbacks);
529 | }
530 | };
531 |
532 | /* Playlist methods. */
533 | this.playlist = {
534 | addTrack : function(params, session, callbacks){
535 | signedCall('playlist.addTrack', params, session, callbacks, 'POST');
536 | },
537 |
538 | create : function(params, session, callbacks){
539 | signedCall('playlist.create', params, session, callbacks, 'POST');
540 | },
541 |
542 | fetch : function(params, callbacks){
543 | call('playlist.fetch', params, callbacks);
544 | }
545 | };
546 |
547 | /* Radio methods. */
548 | this.radio = {
549 | getPlaylist : function(params, session, callbacks){
550 | signedCall('radio.getPlaylist', params, session, callbacks);
551 | },
552 |
553 | search : function(params, session, callbacks){
554 | signedCall('radio.search', params, session, callbacks);
555 | },
556 |
557 | tune : function(params, session, callbacks){
558 | signedCall('radio.tune', params, session, callbacks);
559 | }
560 | };
561 |
562 | /* Tag methods. */
563 | this.tag = {
564 | getInfo : function(params, callbacks){
565 | call('tag.getInfo', params, callbacks);
566 | },
567 |
568 | getSimilar : function(params, callbacks){
569 | call('tag.getSimilar', params, callbacks);
570 | },
571 |
572 | getTopAlbums : function(params, callbacks){
573 | call('tag.getTopAlbums', params, callbacks);
574 | },
575 |
576 | getTopArtists : function(params, callbacks){
577 | call('tag.getTopArtists', params, callbacks);
578 | },
579 |
580 | getTopTags : function(callbacks){
581 | call('tag.getTopTags', null, callbacks);
582 | },
583 |
584 | getTopTracks : function(params, callbacks){
585 | call('tag.getTopTracks', params, callbacks);
586 | },
587 |
588 | getWeeklyArtistChart : function(params, callbacks){
589 | call('tag.getWeeklyArtistChart', params, callbacks);
590 | },
591 |
592 | getWeeklyChartList : function(params, callbacks){
593 | call('tag.getWeeklyChartList', params, callbacks);
594 | },
595 |
596 | search : function(params, callbacks){
597 | call('tag.search', params, callbacks);
598 | }
599 | };
600 |
601 | /* Tasteometer method. */
602 | this.tasteometer = {
603 | compare : function(params, callbacks){
604 | call('tasteometer.compare', params, callbacks);
605 | },
606 |
607 | compareGroup : function(params, callbacks){
608 | call('tasteometer.compareGroup', params, callbacks);
609 | }
610 | };
611 |
612 | /* Track methods. */
613 | this.track = {
614 | addTags : function(params, session, callbacks){
615 | signedCall('track.addTags', params, session, callbacks, 'POST');
616 | },
617 |
618 | ban : function(params, session, callbacks){
619 | signedCall('track.ban', params, session, callbacks, 'POST');
620 | },
621 |
622 | getBuylinks : function(params, callbacks){
623 | call('track.getBuylinks', params, callbacks);
624 | },
625 |
626 | getCorrection : function(params, callbacks){
627 | call('track.getCorrection', params, callbacks);
628 | },
629 |
630 | getFingerprintMetadata : function(params, callbacks){
631 | call('track.getFingerprintMetadata', params, callbacks);
632 | },
633 |
634 | getInfo : function(params, callbacks){
635 | call('track.getInfo', params, callbacks);
636 | },
637 |
638 | getShouts : function(params, callbacks){
639 | call('track.getShouts', params, callbacks);
640 | },
641 |
642 | getSimilar : function(params, callbacks){
643 | call('track.getSimilar', params, callbacks);
644 | },
645 |
646 | getTags : function(params, session, callbacks){
647 | signedCall('track.getTags', params, session, callbacks);
648 | },
649 |
650 | getTopFans : function(params, callbacks){
651 | call('track.getTopFans', params, callbacks);
652 | },
653 |
654 | getTopTags : function(params, callbacks){
655 | call('track.getTopTags', params, callbacks);
656 | },
657 |
658 | love : function(params, session, callbacks){
659 | signedCall('track.love', params, session, callbacks, 'POST');
660 | },
661 |
662 | removeTag : function(params, session, callbacks){
663 | signedCall('track.removeTag', params, session, callbacks, 'POST');
664 | },
665 |
666 | scrobble : function(params, session, callbacks){
667 | /* Flatten an array of multiple tracks into an object with "array notation". */
668 | if(params.constructor.toString().indexOf("Array") != -1){
669 | var p = {};
670 |
671 | for(i in params){
672 | for(j in params[i]){
673 | p[j + '[' + i + ']'] = params[i][j];
674 | }
675 | }
676 |
677 | params = p;
678 | }
679 |
680 | signedCall('track.scrobble', params, session, callbacks, 'POST');
681 | },
682 |
683 | search : function(params, callbacks){
684 | call('track.search', params, callbacks);
685 | },
686 |
687 | share : function(params, session, callbacks){
688 | /* Build comma separated recipients string. */
689 | if(typeof(params.recipient) == 'object'){
690 | params.recipient = params.recipient.join(',');
691 | }
692 |
693 | signedCall('track.share', params, session, callbacks, 'POST');
694 | },
695 |
696 | unban : function(params, session, callbacks){
697 | signedCall('track.unban', params, session, callbacks, 'POST');
698 | },
699 |
700 | unlove : function(params, session, callbacks){
701 | signedCall('track.unlove', params, session, callbacks, 'POST');
702 | },
703 |
704 | updateNowPlaying : function(params, session, callbacks){
705 | signedCall('track.updateNowPlaying', params, session, callbacks, 'POST');
706 | }
707 | };
708 |
709 | /* User methods. */
710 | this.user = {
711 | getArtistTracks : function(params, callbacks){
712 | call('user.getArtistTracks', params, callbacks);
713 | },
714 |
715 | getBannedTracks : function(params, callbacks){
716 | call('user.getBannedTracks', params, callbacks);
717 | },
718 |
719 | getEvents : function(params, callbacks){
720 | call('user.getEvents', params, callbacks);
721 | },
722 |
723 | getFriends : function(params, callbacks){
724 | call('user.getFriends', params, callbacks);
725 | },
726 |
727 | getInfo : function(params, callbacks){
728 | call('user.getInfo', params, callbacks);
729 | },
730 |
731 | getLovedTracks : function(params, callbacks){
732 | call('user.getLovedTracks', params, callbacks);
733 | },
734 |
735 | getNeighbours : function(params, callbacks){
736 | call('user.getNeighbours', params, callbacks);
737 | },
738 |
739 | getNewReleases : function(params, callbacks){
740 | call('user.getNewReleases', params, callbacks);
741 | },
742 |
743 | getPastEvents : function(params, callbacks){
744 | call('user.getPastEvents', params, callbacks);
745 | },
746 |
747 | getPersonalTracks : function(params, callbacks){
748 | call('user.getPersonalTracks', params, callbacks);
749 | },
750 |
751 | getPlaylists : function(params, callbacks){
752 | call('user.getPlaylists', params, callbacks);
753 | },
754 |
755 | getRecentStations : function(params, session, callbacks){
756 | signedCall('user.getRecentStations', params, session, callbacks);
757 | },
758 |
759 | getRecentTracks : function(params, callbacks){
760 | call('user.getRecentTracks', params, callbacks);
761 | },
762 |
763 | getRecommendedArtists : function(params, session, callbacks){
764 | signedCall('user.getRecommendedArtists', params, session, callbacks);
765 | },
766 |
767 | getRecommendedEvents : function(params, session, callbacks){
768 | signedCall('user.getRecommendedEvents', params, session, callbacks);
769 | },
770 |
771 | getShouts : function(params, callbacks){
772 | call('user.getShouts', params, callbacks);
773 | },
774 |
775 | getTopAlbums : function(params, callbacks){
776 | call('user.getTopAlbums', params, callbacks);
777 | },
778 |
779 | getTopArtists : function(params, callbacks){
780 | call('user.getTopArtists', params, callbacks);
781 | },
782 |
783 | getTopTags : function(params, callbacks){
784 | call('user.getTopTags', params, callbacks);
785 | },
786 |
787 | getTopTracks : function(params, callbacks){
788 | call('user.getTopTracks', params, callbacks);
789 | },
790 |
791 | getWeeklyAlbumChart : function(params, callbacks){
792 | call('user.getWeeklyAlbumChart', params, callbacks);
793 | },
794 |
795 | getWeeklyArtistChart : function(params, callbacks){
796 | call('user.getWeeklyArtistChart', params, callbacks);
797 | },
798 |
799 | getWeeklyChartList : function(params, callbacks){
800 | call('user.getWeeklyChartList', params, callbacks);
801 | },
802 |
803 | getWeeklyTrackChart : function(params, callbacks){
804 | call('user.getWeeklyTrackChart', params, callbacks);
805 | },
806 |
807 | shout : function(params, session, callbacks){
808 | signedCall('user.shout', params, session, callbacks, 'POST');
809 | }
810 | };
811 |
812 | /* Venue methods. */
813 | this.venue = {
814 | getEvents : function(params, callbacks){
815 | call('venue.getEvents', params, callbacks);
816 | },
817 |
818 | getPastEvents : function(params, callbacks){
819 | call('venue.getPastEvents', params, callbacks);
820 | },
821 |
822 | search : function(params, callbacks){
823 | call('venue.search', params, callbacks);
824 | }
825 | };
826 |
827 | /* Private auth methods. */
828 | var auth = {
829 | getApiSignature : function(params){
830 | var keys = [];
831 | var string = '';
832 |
833 | for(var key in params){
834 | keys.push(key);
835 | }
836 |
837 | keys.sort();
838 |
839 | for(var index in keys){
840 | var key = keys[index];
841 |
842 | string += key + params[key];
843 | }
844 |
845 | string += apiSecret;
846 |
847 | /* Needs lastfm.api.md5.js. */
848 | return hex_md5(string);
849 | }
850 | };
851 | }
852 |
--------------------------------------------------------------------------------
/source/js/libs/md5-min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
3 | * Digest Algorithm, as defined in RFC 1321.
4 | * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
6 | * Distributed under the BSD License
7 | * See http://pajhome.org.uk/crypt/md5 for more info.
8 | */
9 | var hexcase=0;function hex_md5(a){return rstr2hex(rstr_md5(str2rstr_utf8(a)))}function hex_hmac_md5(a,b){return rstr2hex(rstr_hmac_md5(str2rstr_utf8(a),str2rstr_utf8(b)))}function md5_vm_test(){return hex_md5("abc").toLowerCase()=="900150983cd24fb0d6963f7d28e17f72"}function rstr_md5(a){return binl2rstr(binl_md5(rstr2binl(a),a.length*8))}function rstr_hmac_md5(c,f){var e=rstr2binl(c);if(e.length>16){e=binl_md5(e,c.length*8)}var a=Array(16),d=Array(16);for(var b=0;b<16;b++){a[b]=e[b]^909522486;d[b]=e[b]^1549556828}var g=binl_md5(a.concat(rstr2binl(f)),512+f.length*8);return binl2rstr(binl_md5(d.concat(g),512+128))}function rstr2hex(c){try{hexcase}catch(g){hexcase=0}var f=hexcase?"0123456789ABCDEF":"0123456789abcdef";var b="";var a;for(var d=0;d>>4)&15)+f.charAt(a&15)}return b}function str2rstr_utf8(c){var b="";var d=-1;var a,e;while(++d>>6)&31),128|(a&63))}else{if(a<=65535){b+=String.fromCharCode(224|((a>>>12)&15),128|((a>>>6)&63),128|(a&63))}else{if(a<=2097151){b+=String.fromCharCode(240|((a>>>18)&7),128|((a>>>12)&63),128|((a>>>6)&63),128|(a&63))}}}}}return b}function rstr2binl(b){var a=Array(b.length>>2);for(var c=0;c>5]|=(b.charCodeAt(c/8)&255)<<(c%32)}return a}function binl2rstr(b){var a="";for(var c=0;c>5]>>>(c%32))&255)}return a}function binl_md5(p,k){p[k>>5]|=128<<((k)%32);p[(((k+64)>>>9)<<4)+14]=k;var o=1732584193;var n=-271733879;var m=-1732584194;var l=271733878;for(var g=0;g>16)+(d>>16)+(c>>16);return(b<<16)|(c&65535)}function bit_rol(a,b){return(a<>>(32-b))};
--------------------------------------------------------------------------------
/source/js/pages/background.js:
--------------------------------------------------------------------------------
1 | chrome.runtime.onMessage.addListener((request) => {
2 | const {id, videoSrc, videoTitle} = request;
3 | const nameChunks = videoSrc.split('.');
4 | const extension = nameChunks[nameChunks.length - 1];
5 |
6 | if (id === 'video') {
7 | chrome.downloads.download({
8 | url: videoSrc,
9 | filename: `${videoTitle}.${extension}`,
10 | });
11 | }
12 | });
--------------------------------------------------------------------------------
/source/js/pages/lastauth.js:
--------------------------------------------------------------------------------
1 | window.onload = () => {
2 | const token = window.location.search.replace('?token=', '');
3 | const storage = chrome.storage.sync;
4 | let userName = document.querySelector('.user');
5 |
6 | storage.get('lastkeys', (data) => {
7 | const apiKey = data.lastkeys.api;
8 | const apiSecret = data.lastkeys.secret;
9 | let lastfm = new LastFM({
10 | apiKey: apiKey,
11 | apiSecret: apiSecret
12 | });
13 | lastfm.auth.getSession({
14 | token: token
15 | }, {
16 | success: (data) => {
17 |
18 | storage.set({'lastsession': data.session.key}, () => {
19 | chrome.tabs.query({'url': '*://vk.com/*'}, (tabs) => {
20 | tabs.forEach( (tab) => {
21 | chrome.tabs.executeScript(tab.id, {
22 | file: "js/vk-observer.js"
23 | });
24 | });
25 | });
26 | userName.innerHTML = data.session.name + ',' + userName.innerHTML;
27 | }
28 | )
29 | },
30 | error: (code, message) => {
31 | console.log("Ошибка: " + message + " код: " + code);
32 | }
33 | });
34 | });
35 | };
--------------------------------------------------------------------------------
/source/js/pages/popup.js:
--------------------------------------------------------------------------------
1 | const storage = chrome.storage.sync;
2 |
3 | let saveOption = (value) => {
4 | storage.set({'settings': value}, () => {
5 | chrome.tabs.query({'url': '*://vk.com/*'}, (tabs) => {
6 | tabs.forEach( (tab) => {
7 | chrome.tabs.executeScript(tab.id, {file: "js/vk-observer.js"});
8 | })
9 | });
10 | });
11 | };
12 |
13 | let saveLastKeys = (defaultKeys) => {
14 | storage.set({'lastkeys': defaultKeys}, () => {
15 | chrome.tabs.query({'url': '*://vk.com/*'}, (tabs) => {
16 | tabs.forEach( (tab, k) => {
17 | chrome.tabs.executeScript(tabs[k].id, {file: "js/vk-observer.js"});
18 | })
19 | });
20 | });
21 | };
22 |
23 | let getOption = () => {
24 | // let toggleCache = document.querySelector('.toggle-cache'),
25 | let toggleBitrate = document.querySelector('.toggle-bitrate'),
26 | toggleScrobble = document.querySelector('.toggle-scrobble');
27 | storage.get('settings', (data) => {
28 | let state = data.settings,
29 | defaultSettings = {
30 | "bitrate": 'enabled',
31 | // "cache": 'enabled',
32 | "scrobble": 'disabled'
33 | };
34 |
35 | if (state === undefined || state.scrobble === undefined) {
36 | storage.clear();
37 | saveOption(defaultSettings);
38 | }
39 | // if (state.cache === 'enabled') {
40 | // toggleCache.checked = true;
41 | // }
42 | // if (state.cache === 'disabled') {
43 | // toggleCache.checked = false;
44 | // }
45 | if (state.bitrate === 'enabled') {
46 | toggleBitrate.checked = true;
47 | }
48 | if (state.bitrate === 'disabled') {
49 | toggleBitrate.checked = false;
50 | }
51 |
52 | if (state.scrobble === 'enabled') {
53 | toggleScrobble.checked = true;
54 | }
55 | if (state.scrobble === 'disabled') {
56 | toggleScrobble.checked = false;
57 | }
58 |
59 |
60 | });
61 | storage.get('lastkeys', (data) => {
62 | const keys = data.lastkeys;
63 | const defaultKeys = {
64 | "api": '488c3ca0fd22d7b5e8a0cd9650322d33',
65 | "secret": '783d2f7628431a0c954381cf3c342575'
66 | };
67 | if (keys === undefined) {
68 | saveLastKeys(defaultKeys);
69 | }
70 | });
71 | };
72 |
73 | window.onload = () => {
74 | // let switcherCache = document.querySelector('.switcher-cache'),
75 | // toggleCache = document.querySelector('.toggle-cache'),
76 | let switcherBitrate = document.querySelector('.switcher-bitrate'),
77 | toggleBitrate = document.querySelector('.toggle-bitrate'),
78 | switcherScrobble = document.querySelector('.switcher-scrobble'),
79 | toggleScrobble = document.querySelector('.toggle-scrobble');
80 |
81 | // let changeCache = (event) => {
82 | // let bitrateStatus = '',
83 | // scrobbleStatus = '';
84 |
85 | // bitrateStatus = toggleBitrate.checked ? 'enabled' : 'disabled';
86 | // scrobbleStatus = toggleScrobble.checked ? 'enabled' : 'disabled';
87 |
88 | // storage.get('settings', (data) => {
89 | // let state = data.settings,
90 | // options = {
91 | // bitrate: bitrateStatus,
92 | // cache: 'enabled',
93 | // scrobble: scrobbleStatus
94 | // };
95 | // toggleCache.checked = true;
96 | // if (state.cache === 'enabled') {
97 | // toggleCache.checked = false;
98 | // options.cache = 'disabled';
99 | // }
100 | // saveOption(options);
101 | // });
102 | // };
103 |
104 | let changeBitrate = (event) => {
105 | // let cacheStatus = '',
106 | let scrobbleStatus = '';
107 |
108 | // cacheStatus = toggleCache.checked ? 'enabled' : 'disabled';
109 | scrobbleStatus = toggleScrobble.checked ? 'enabled' : 'disabled';
110 |
111 | storage.get('settings', function(data) {
112 | var state = data.settings,
113 | options = {
114 | "bitrate": 'enabled',
115 | // "cache": cacheStatus,
116 | "scrobble": scrobbleStatus
117 | };
118 | toggleBitrate.checked = true;
119 | if (state.bitrate === 'enabled') {
120 | toggleBitrate.checked = false;
121 | options.bitrate = 'disabled';
122 | }
123 | saveOption(options);
124 | });
125 | };
126 |
127 | let changeScrobble = (event) => {
128 | // let cacheStatus = '',
129 | let bitrateStatus = '';
130 |
131 | // cacheStatus = toggleCache.checked ? 'enabled' : 'disabled';
132 | bitrateStatus = toggleBitrate.checked ? 'enabled' : 'disabled';
133 |
134 | storage.get('settings', (data) => {
135 | var state = data.settings,
136 | options = {
137 | "bitrate": bitrateStatus,
138 | // "cache": cacheStatus,
139 | "scrobble": 'enabled'
140 | };
141 | toggleScrobble.checked = true;
142 | if (state.scrobble === 'enabled') {
143 | toggleScrobble.checked = false;
144 | options.scrobble = 'disabled';
145 | storage.remove('lastsession', () => {
146 | saveOption(options);
147 | })
148 | }
149 | saveOption(options);
150 | });
151 |
152 | storage.get('lastsession', (data) => {
153 | const sessionKey = data.lastsession,
154 | apiKey = '488c3ca0fd22d7b5e8a0cd9650322d33';
155 | let apiUrl = 'http://www.lastfm.ru/api/auth?api_key=' + apiKey;
156 |
157 | if (sessionKey === undefined) {
158 | chrome.tabs.create({url:apiUrl});
159 | }
160 |
161 | });
162 |
163 | };
164 |
165 | getOption();
166 | // switcherCache.addEventListener('click', changeCache, false);
167 | switcherBitrate.addEventListener('click', changeBitrate, false);
168 | switcherScrobble.addEventListener('click', changeScrobble, false);
169 | }
170 |
--------------------------------------------------------------------------------
/source/js/pages/startup.js:
--------------------------------------------------------------------------------
1 | const navigatorModifier = '(' + function() {
2 | window.MediaSource = null
3 | } + ')();';
4 |
5 | const modifierScript = document.createElement('script');
6 |
7 | modifierScript.textContent = navigatorModifier;
8 | document.documentElement.appendChild(modifierScript);
9 | modifierScript.remove();
10 |
--------------------------------------------------------------------------------
/source/js/utils/index.js:
--------------------------------------------------------------------------------
1 | export function declension(integer, titles, highlight=true, isFull=true) {
2 | const number = Math.abs(integer)
3 | const cases = [2, 0, 1, 1, 1, 2]
4 | const text = titles[
5 | (number % 100 > 4 && number % 100 < 20)
6 | ?
7 | 2
8 | :
9 | cases[(number % 10 < 5) ? number % 10 : 5]
10 | ]
11 |
12 | return isFull
13 | ?
14 | `${ highlight ? `${ integer }` : integer } ${ text }`
15 | :
16 | `${ text }`
17 | }
18 |
19 | export function getJSON(str) {
20 | try {
21 | return JSON.parse(str)
22 | } catch(err) {
23 | return {}
24 | }
25 | }
26 |
27 | export function hash() {
28 | return Math.random().toString(16).slice(2, 10)
29 | }
30 |
31 | export function elementsActive(el, initial='active') {
32 | if(Array.isArray(el)) {
33 | el.forEach(
34 | item => item.className = item.className.indexOf(initial) >= 0
35 | )
36 | } else {
37 | el.className = el.className.indexOf(initial) >= 0
38 | }
39 | }
40 |
41 | export function toggleActive(el, classes={
42 | initial: 'active',
43 | active: ' active'
44 | }) {
45 | const { initial, active } = classes
46 |
47 | if(Array.isArray(el)) {
48 | Array.prototype.slice.call(el).forEach(
49 | item => item.className = item.className.indexOf(initial) >= 0
50 | ?
51 | item.className.replace(active, '')
52 | :
53 | `${ item.className }${ active }`
54 | )
55 | } else {
56 | el.className = el.className.indexOf(initial) >= 0
57 | ?
58 | el.className.replace(active, '')
59 | :
60 | `${ el.className }${ active }`
61 | }
62 |
63 | }
64 |
65 | export function xhr(options) {
66 | const {
67 | url,
68 | method='GET',
69 | body=null,
70 | headers=[],
71 | responseType,
72 | optional={},
73 | onProgress = () => {},
74 | } = options
75 | const isBlob = responseType && responseType === 'blob';
76 | const calculateBitrate = optional && optional.calculateBitrate;
77 |
78 | return new Promise(
79 | (resolve, reject) => {
80 | const request = new XMLHttpRequest();
81 | let validStatus = 200;
82 |
83 | request.open(method, url)
84 |
85 | if(headers.length > 0) {
86 | headers.forEach(header =>
87 | request.setRequestHeader(header.name, header.value)
88 | )
89 | }
90 |
91 | if(responseType) {
92 | request.responseType = responseType
93 | }
94 |
95 | if(calculateBitrate) {
96 | validStatus = 206;
97 | }
98 |
99 | request.onprogress = onProgress
100 |
101 | request.onreadystatechange = function() {
102 | if(this.readyState === 4) {
103 | if(this.status === validStatus) {
104 | if(calculateBitrate) {
105 | const duration = optional.duration;
106 | const contentRange = request.getResponseHeader('Content-Range')
107 | const size = contentRange.split('/').pop();
108 | const sizeLong = Math.floor(size / 1024) / 1024;
109 | const sizeShort = sizeLong.toFixed(1);
110 | const kbit = size / 128;
111 | let kbps = Math.ceil(Math.round(kbit / duration) / 16) * 16;
112 |
113 | if (kbps > 320) {
114 | kbps = 320;
115 | }
116 |
117 | optional.bitrate = { kbit, kbps };
118 | optional.fileSize = sizeShort;
119 |
120 | }
121 |
122 | resolve(
123 | isBlob ?
124 | this.response :
125 | {
126 | result: this.response,
127 | optional
128 | }
129 | );
130 | } else {
131 | const { status, statusText } = this;
132 |
133 | console.error('XHR Helper', status, statusText);
134 | reject({status, statusText});
135 | }
136 | }
137 | }
138 |
139 | request.send(body)
140 | }
141 | )
142 | }
143 |
144 | export function isJSON(json) {
145 | try {
146 | const obj = JSON.parse(json)
147 | if (obj && typeof obj === 'object' && obj !== null) {
148 | return true
149 | }
150 | } catch (err) {}
151 | return false
152 | }
153 |
154 | export function isFunction(func) {
155 | return Object.prototype.toString.call(func) === '[object Function]'
156 | }
157 |
158 | export function isArray(arr) {
159 | return Object.prototype.toString.call(arr) === '[object Array]'
160 | }
161 |
162 | export function decodeURL(t, userId) {
163 | function o(encodedURL) {
164 | if (~encodedURL.indexOf('audio_api_unavailable')) {
165 |
166 | let params = encodedURL.split("?extra=")[1].split("#");
167 | let additionalParams = '' === params[1] ? '' : mapper(params[1]);
168 |
169 | params = mapper(params[0]);
170 |
171 | if (typeof additionalParams != 'string' || !params) return encodedURL;
172 |
173 | additionalParams = additionalParams ? additionalParams.split(String.fromCharCode(9)) : [];
174 |
175 | for (let a, r, length = additionalParams.length; length--; ) {
176 | r = additionalParams[length].split(String.fromCharCode(11));
177 | a = r.splice(0, 1, params)[0];
178 |
179 | if (!stringModifier[a]) return encodedURL;
180 |
181 | params = stringModifier[a].apply(null, r)
182 | }
183 |
184 | if (params && "http" === params.substr(0, 4)) return params;
185 |
186 | }
187 |
188 | return encodedURL;
189 | }
190 |
191 | function mapper(params) {
192 | let r = "";
193 |
194 | if (!params || params.length % 4 === 1) return !1;
195 |
196 | for (let t, i, a = 0, o = 0; i = params.charAt(o++);) {
197 | i = vocabulary.indexOf(i);
198 |
199 | ~i
200 | &&
201 | (t = a % 4 ? 64 * t + i : i, a++ % 4)
202 | &&
203 | (r += String.fromCharCode(255 & t >> (-2 * a & 6)));
204 | }
205 |
206 | return r;
207 | }
208 |
209 | function bin(t, e) {
210 | const o = [];
211 |
212 | if (t.length) {
213 | let a = t.length;
214 | for (e = Math.abs(e); a--; )
215 | e = (t.length * (a + 1) ^ e + a) % t.length,
216 | o[a] = e
217 | }
218 | return o
219 | }
220 |
221 | const vocabulary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=";
222 | const stringModifier = {
223 | v: (t) => t.split("").reverse().join(""),
224 | r: (string, i) => {
225 | string = string.split("");
226 |
227 | for (let e, o = vocabulary + vocabulary, length = string.length; length--; )
228 | e = o.indexOf(string[length]),
229 | ~e && (string[length] = o.substr(e - i, 1));
230 |
231 | return string.join("")
232 | },
233 | s: (string, i) => {
234 | if (string.length) {
235 | const binData = bin(string, i);
236 | let a = 0;
237 |
238 | for (string = string.split(''); ++a < string.length;) {
239 | const startIndex = binData[string.length - 1 - a];
240 | const amount = 1;
241 |
242 | string[a] = string.splice(startIndex, amount, string[a])[0];
243 | }
244 |
245 | string = string.join('');
246 | }
247 |
248 | return string;
249 | },
250 | i: (t, e) => stringModifier.s(t, e ^ userId),
251 | x: (t, e) => {
252 | var i = [];
253 | return e = e.charCodeAt(0),
254 | each(t.split(""), function(t, o) {
255 | i.push(String.fromCharCode(o.charCodeAt(0) ^ e))
256 | }),
257 | i.join("")
258 | }
259 | };
260 |
261 | return o(t);
262 | }
263 |
264 | export function defineVideoQuality(quality) {
265 | let definition;
266 |
267 | switch (quality) {
268 | case 240:
269 | definition = 'SD';
270 | break;
271 | case 360:
272 | definition = 'SD';
273 | break;
274 | case 480:
275 | definition = 'SD';
276 | break;
277 | case 720:
278 | definition = 'HD';
279 | break;
280 | default:
281 | definition = 'FullHD';
282 | break;
283 | }
284 |
285 | return `Загрузить - ${ definition }`;
286 | }
287 |
--------------------------------------------------------------------------------
/source/less/lastauth.less:
--------------------------------------------------------------------------------
1 | body {
2 | background: url(../images/pattern.png);
3 | font-family: 'Arial', sans-serif;
4 | color: #fff;
5 | }
6 |
7 | .content {
8 | display: table;
9 | width: 1000px;
10 | min-height: 500px;
11 | margin: auto;
12 | }
13 |
14 | .wrap {
15 | display: table-cell;
16 | vertical-align: middle;
17 | text-align: center;
18 | }
19 |
20 | h1 {
21 | font-weight: normal;
22 | font-size: 25px;
23 | }
24 |
25 | .user {
26 | text-transform: uppercase;
27 | font-size: 25px;
28 | font-weight: bold;
29 | letter-spacing: 2px;
30 | }
31 |
32 | .logo {
33 | display: inline-block;
34 | vertical-align: top;
35 | }
--------------------------------------------------------------------------------
/source/less/popup.less:
--------------------------------------------------------------------------------
1 | body {
2 | width: 165px;
3 | background: #D7D8D9;
4 | }
5 |
6 | .switcher {
7 | padding: 5px 0;
8 | }
9 |
10 | .switcher,
11 | .switcher-label {
12 | display: inline-block;
13 | vertical-align: middle;
14 | }
15 |
16 | .switcher-label h4 {
17 | font-size: 16px;
18 | margin: 0;
19 | font-family: 'Arial', sans-serif;
20 | font-weight: bold;
21 | background-color: #969696;
22 | -webkit-background-clip: text;
23 | -moz-background-clip: text;
24 | background-clip: text;
25 | color: rgba(0, 0, 0, 0.35);
26 | text-shadow: rgba(255, 255, 255, 0.8) 1px 1px 1px;
27 | }
28 |
29 | input.toggle-scrobble,
30 | input.toggle-cache,
31 | input.toggle-bitrate {
32 | display: none;
33 | max-height: 0;
34 | max-width: 0;
35 | opacity: 0;
36 | }
37 |
38 | input.toggle-scrobble + label,
39 | input.toggle-cache + label,
40 | input.toggle-bitrate + label {
41 | display: block;
42 | position: relative;
43 | background: rgba(241, 172, 172, 0.9);
44 | box-shadow: inset 0 0 1px 1px rgba(148, 148, 148, 0.35);
45 | text-indent: -5000px;
46 | height: 30px;
47 | width: 50px;
48 | border-radius: 15px;
49 | cursor: pointer;
50 | }
51 |
52 | input.toggle-scrobble + label:before,
53 | input.toggle-cache + label:before,
54 | input.toggle-bitrate + label:before {
55 | content: "";
56 | position: absolute;
57 | display: block;
58 | height: 30px;
59 | width: 30px;
60 | top: 0;
61 | left: 0;
62 | border-radius: 15px;
63 | background: rgba(19,191,17,0);
64 | -moz-transition: .25s ease-in-out;
65 | -webkit-transition: .25s ease-in-out;
66 | transition: .25s ease-in-out;
67 | }
68 |
69 | input.toggle-scrobble + label:after,
70 | input.toggle-cache + label:after,
71 | input.toggle-bitrate + label:after {
72 | content: "";
73 | position: absolute;
74 | display: block;
75 | height: 30px;
76 | width: 30px;
77 | top: 0;
78 | left: 0px;
79 | border-radius: 15px;
80 | background: #c4c4c4 -webkit-gradient( linear,0% 0%,0% 100%,from(rgba(255,255,255,.4)),to(rgba(0,0,0,0)));
81 | box-shadow: inset 0 0 0 1px rgba(0,0,0,.2), 0 2px 4px rgba(0,0,0,.2);
82 | -moz-transition: .25s ease-in-out;
83 | -webkit-transition: .25s ease-in-out;
84 | transition: .25s ease-in-out;
85 | }
86 |
87 | input.toggle-scrobble:checked + label:before,
88 | input.toggle-cache:checked + label:before,
89 | input.toggle-bitrate:checked + label:before {
90 | width: 50px;
91 | background: #96B2CC;
92 | }
93 |
94 | input.toggle-scrobble:checked + label:after,
95 | input.toggle-cache:checked + label:after,
96 | input.toggle-bitrate:checked + label:after {
97 | left: 20px;
98 | box-shadow: inset 0 0 0 1px #96B2CC, 0 2px 4px rgba(0,0,0,.2);
99 | }
100 |
101 | /* Social Likes */
102 | .social-likes,
103 | .social-likes__widget {
104 | display: inline-block;
105 | padding: 0;
106 | vertical-align: middle !important;
107 | word-spacing: 0 !important;
108 | text-indent: 0 !important;
109 | list-style: none !important;
110 | outline: none;
111 | }
112 |
113 | .social-likes {
114 | opacity: 0;
115 | }
116 |
117 | .social-likes_visible {
118 | opacity: 1;
119 | -webkit-transition: opacity .1s ease-in;
120 | transition: opacity .1s ease-in;
121 | }
122 |
123 | .social-likes>* {
124 | display: inline-block;
125 | visibility: hidden;
126 | }
127 |
128 | .social-likes_visible>* {
129 | visibility: inherit;
130 | }
131 |
132 | .social-likes__widget {
133 | display: inline-block;
134 | position: relative;
135 | white-space: nowrap;
136 | text-shadow: rgba(255, 255, 255, 0.8) 1px 1px 1px;
137 | font-size: 15px;
138 | }
139 |
140 | .social-likes__widget:before, .social-likes__widget:after {
141 | display: none !important;
142 | }
143 |
144 | .social-likes__button, .social-likes__icon {
145 | text-decoration: none;
146 | text-rendering: optimizeLegibility;
147 | }
148 |
149 | .social-likes__button {
150 | display: inline-block;
151 | margin: 0;
152 | outline: 0;
153 | }
154 |
155 | .social-likes__button {
156 | position: relative;
157 | cursor: pointer;
158 | -webkit-user-select: none;
159 | -moz-user-select: none;
160 | -ms-user-select: none;
161 | user-select: none;
162 | }
163 |
164 | .social-likes__button:before {
165 | content: "";
166 | display: inline-block;
167 | }
168 |
169 | .social-likes__icon {
170 | position: absolute;
171 | }
172 |
173 | .social-likes_notext .social-likes__button {
174 | padding-left: 0;
175 | }
176 |
177 | .social-likes_single-w {
178 | position: relative;
179 | display: inline-block;
180 | }
181 |
182 | .social-likes_single {
183 | position: absolute;
184 | text-align: left;
185 | z-index: 99999;
186 | visibility: hidden;
187 | opacity: 0;
188 | -webkit-transition: visibility 0 .11s, opacity .1s ease-in;
189 | transition: visibility 0s .11s, opacity .1s ease-in;
190 | -webkit-backface-visibility: hidden;
191 | backface-visibility: hidden;
192 | }
193 |
194 | .social-likes_single.social-likes_opened {
195 | visibility: visible;
196 | opacity: 1;
197 | -webkit-transition: opacity .15s ease-out;
198 | transition: opacity .15s ease-out;
199 | }
200 |
201 | .social-likes__button_single {
202 | position: relative;
203 | }
204 |
205 | @font-face {
206 | font-family: "social-likes";
207 | src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff");
208 | font-weight: 400;
209 | font-style: normal;
210 | }
211 |
212 | .social-likes__icon_vkontakte:before {
213 | content: "\f109";
214 | }
215 |
216 | .social-likes {
217 | min-height: 36px;
218 | margin: -.5em;
219 | -webkit-transform: translate3d(0, 0, 0);
220 | transform: translate3d(0, 0, 0);
221 | }
222 |
223 | .social-likes, .social-likes_single-w {
224 | line-height: 1.5;
225 | }
226 |
227 | .social-likes, .social-likes__widget_single {
228 | font-size: 14px;
229 | }
230 |
231 | .social-likes__widget {
232 | margin: .8em;
233 | line-height: 1.5;
234 | border: 0;
235 | text-align: left;
236 | cursor: pointer;
237 | }
238 |
239 | .social-likes__button {
240 | -moz-box-sizing: border-box;
241 | box-sizing: border-box;
242 | font-family: 'Arial', sans-serif;
243 | vertical-align: baseline;
244 | color: #fff;
245 | }
246 |
247 | .social-likes__button {
248 | padding: .04em .7em .18em 1.65em;
249 | font-weight: 700;
250 | -webkit-font-smoothing: antialiased;
251 | -moz-osx-font-smoothing: grayscale;
252 | }
253 |
254 | .social-likes__icon {
255 | top: 0;
256 | left: .21em;
257 | font-family: "social-likes";
258 | font-weight: 400;
259 | font-style: normal;
260 | speak: none;
261 | text-transform: none;
262 | font-size: 1.35em;
263 | vertical-align: baseline;
264 | }
265 |
266 | .social-likes_vertical .social-likes__widget {
267 | min-width: 13em;
268 | }
269 |
270 | .social-likes_light .social-likes__widget {
271 | min-width: 0;
272 | background: 0 0;
273 | }
274 |
275 | .social-likes_light .social-likes__button, .social-likes_single-light+.social-likes__button {
276 | min-width: 0;
277 | padding-left: 1.35em;
278 | font-weight: 400;
279 | -webkit-font-smoothing: subpixel-antialiased;
280 | -moz-osx-font-smoothing: auto;
281 | }
282 |
283 | .social-likes_light .social-likes__icon {
284 | margin-top: -.1em;
285 | margin-left: -.25em;
286 | }
287 |
288 | .social-likes_notext .social-likes__button {
289 | width: 1.85em;
290 | }
291 |
292 | .social-likes_notext .social-likes__icon {
293 | margin-left: .1em;
294 | }
295 |
296 | .social-likes_notext.social-likes_light, .social-likes_notext.social-likes_light .social-likes__widget, .social-likes_notext.social-likes_light .social-likes__icon {
297 | margin: 0;
298 | left: 0;
299 | }
300 |
301 | .social-likes_notext.social-likes_light .social-likes__button {
302 | width: 1.4em;
303 | padding-left: 0;
304 | }
305 |
306 | .social-likes_single {
307 | margin-top: -1.2em;
308 | padding: .5em;
309 | background: #fff;
310 | border: 1px solid #ddd;
311 | }
312 |
313 | .social-likes__widget_single {
314 | height: 1.7em;
315 | margin: 0;
316 | padding: .1em 0;
317 | line-height: 1.5;
318 | background: #007aff;
319 | }
320 |
321 | .social-likes_single-light+.social-likes__widget_single {
322 | color: #007aff;
323 | }
324 |
325 | .social-likes__icon_single {
326 | left: .4em;
327 | font-size: 1.1em;
328 | }
329 |
330 | .social-likes_light .social-likes__button_vkontakte {
331 | background-color: #969696;
332 | -webkit-background-clip: text;
333 | background-clip: text;
334 | color: rgba(0, 0, 0, 0.35);
335 | }
336 |
337 | .social-likes_light .social-likes__button_vkontakte:hover {
338 | color: #587e9f;
339 | transition: all 0.8s ease-in-out;
340 | }
341 |
342 | .social-likes__icon_vkontakte {
343 | top: .2em;
344 | left: .25em;
345 | }
346 |
--------------------------------------------------------------------------------
/source/less/style.less:
--------------------------------------------------------------------------------
1 | #audio > #ac,
2 | .audio_fixed_nav #page_header {
3 | z-index: 201;
4 | }
5 |
6 | .download-link,
7 | .download-icon {
8 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAMxJREFUeNpi1I6oYsACbIC4EYjrgfgIuiQTA3agCMROUJqBWE3/0GiiNL1Eo1EACxrfGGq6OpQPot9CDT+LSxPI895A/BfKnwjEzEC8FYh9cDmvBqqBGcpnhvJr8PnpAhBnoollQsVRNCkBsQSS2Gwg3g9l74fyYQCkTgmk6RwQV6KZ7gXE/VAaGYDUnQMFBD8Q+wPxK6jNjED8EYjPA3E6VP4/NFRB6vhBmr4BsTwQtzAQB76BNK0CYimkEPsPtY0RyobxGaAh+QwgwAByCyhkqLb69QAAAABJRU5ErkJggg==');
9 | background-repeat: no-repeat;
10 | width: 13px;
11 | height: 13px;
12 | }
13 |
14 | .download-icon,
15 | .arr_div ul {
16 | display: inline-block;
17 | }
18 |
19 | .download-icon {
20 | margin-right: 5px;
21 | }
22 |
23 | .wall_audio_rows .download-link {
24 | top: 13px;
25 | }
26 |
27 | .video_btn {
28 | position: relative;
29 |
30 | &:before {
31 | position: absolute;
32 | top: 3px;
33 | left: -18px;
34 | display: inline-block;
35 | vertical-align: middle;
36 | height: 20px;
37 | width: 20px;
38 | background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDU4IDU4IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1OCA1ODsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSI1MTJweCIgaGVpZ2h0PSI1MTJweCI+CjxnPgoJPHBhdGggZD0iTTI5LDU2aDI0VjMySDI5VjU2eiBNMzEsMzRoOXYxMi41ODZsLTQuMjkzLTQuMjkzbC0xLjQxNCwxLjQxNEw0MSw1MC40MTRsNi43MDctNi43MDdsLTEuNDE0LTEuNDE0TDQyLDQ2LjU4NlYzNGg5djIwSDMxICAgVjM0eiIgZmlsbD0iIzhhOTE5ZiIvPgoJPHBhdGggZD0iTTM3LDI1YzAtMC4zNDItMC4xNzUtMC42Ni0wLjQ2My0wLjg0NGwtMTEtN2MtMC4zMDktMC4xOTUtMC42OTgtMC4yMDgtMS4wMTktMC4wMzNDMjQuMTk5LDE3LjI5OSwyNCwxNy42MzUsMjQsMTh2MTQgICBjMCwwLjM2NSwwLjE5OSwwLjcwMSwwLjUxOSwwLjg3N0MyNC42NjksMzIuOTU5LDI0LjgzNCwzMywyNSwzM2MwLjE4NywwLDAuMzc0LTAuMDUzLDAuNTM3LTAuMTU2bDExLTcgICBDMzYuODI1LDI1LjY2LDM3LDI1LjM0MiwzNywyNXogTTI2LDMwLjE3OVYxOS44MjFMMzQuMTM3LDI1TDI2LDMwLjE3OXoiIGZpbGw9IiM4YTkxOWYiLz4KCTxwYXRoIGQ9Ik01NywySDQ3SDExSDFDMC40NDgsMiwwLDIuNDQ3LDAsM3YxMXYxMXYxMXYxMWMwLDAuNTUzLDAuNDQ4LDEsMSwxaDEwaDEzYzAuNTUyLDAsMS0wLjQ0NywxLTFzLTAuNDQ4LTEtMS0xSDEyVjM2VjI1VjE0VjQgICBoMzR2MTB2MTFjMCwwLjU1MywwLjQ0OCwxLDEsMWg5djhjMCwwLjU1MywwLjQ0NywxLDEsMXMxLTAuNDQ3LDEtMXYtOVYxNFYzQzU4LDIuNDQ3LDU3LjU1MywyLDU3LDJ6IE0yLDI2aDh2OUgyVjI2eiBNMTAsMjRIMnYtOSAgIGg4VjI0eiBNMiw0NnYtOWg4djlIMnogTTEwLDEzSDJWNGg4VjEzeiBNNTYsNHY5aC04VjRINTZ6IE00OCwyNHYtOWg4djlINDh6IiBmaWxsPSIjOGE5MTlmIi8+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==);
39 | background-repeat: no-repeat;
40 | background-size: cover;
41 | content: '';
42 | }
43 | }
44 |
45 | .download-link {
46 | position: relative;
47 | top: 17px;
48 | float: left;
49 | }
50 | .audio_row .audio_row__performer_title {
51 | padding-left: 5px;
52 | }
53 |
54 | .download-link {
55 | // position: absolute;
56 | // right: 2px;
57 | // top: 11px;
58 | z-index: 100;
59 | opacity: 0.4;
60 |
61 | &:hover {
62 | opacity: 1;
63 | transition: all 0.3s ease-in-out;
64 | }
65 | }
66 |
67 | .im_log_body .download-link{
68 | z-index: 100;
69 | }
70 |
71 | .wall_audio_rows {
72 |
73 | // .download-link {
74 | // top: 11px;
75 | // right: 90px;
76 | // }
77 |
78 | .download-all-link {
79 |
80 | .download-tooltip {
81 | right: -5px;
82 | }
83 |
84 | }
85 |
86 | }
87 |
88 | .eltt.top_audio_layer {
89 | .canadd {
90 | .download-link {
91 | right: 15%;
92 | }
93 | }
94 |
95 | .download-link {
96 | top: 11px;
97 | right: 11%;
98 | }
99 |
100 | }
101 |
102 | ._audio_padding_cont {
103 | .download-link {
104 | right: 20.5%;
105 | }
106 | }
107 |
108 | .download-all-link {
109 | position: relative;
110 | display: block;
111 | color: #2B587A;
112 | opacity: .7;
113 | text-align: right;
114 | margin: 0 7px 7px 0;
115 | padding-right: 15px;
116 | background: url(/images/icons/mono_iconset.gif?8) no-repeat 0px -219px;
117 | background-position-x: 100%;
118 |
119 | .download-tooltip {
120 | display: none;
121 | position: absolute;
122 | right: 0;
123 | bottom: 20px;
124 | font-size: 13px;
125 | text-align: center;
126 | color: #FFF;
127 | background: rgba(0, 0, 0, 0.7);
128 | border-radius: 5px;
129 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
130 | padding: 5px;
131 | width: 150px;
132 | text-shadow: 0px 1px 0px #262626;
133 | z-index: 200;
134 |
135 | &:after {
136 | position: absolute;
137 | display: block;
138 | content: '';
139 | right: 25px;
140 | bottom: -7px;
141 | background: url(/images/icons/like_icons_bl.png) -2px -47px no-repeat;
142 | width: 13px;
143 | height: 7px;
144 | }
145 | }
146 |
147 | &:hover {
148 | opacity: 1;
149 | text-decoration: none;
150 |
151 | .download-tooltip {
152 | display: block;
153 | }
154 |
155 | }
156 | }
157 |
158 | .audio_row {
159 | .audio_row_cover_wrap {
160 | width: 62px !important;
161 | background-repeat: no-repeat;
162 | }
163 | //FIXME
164 | &[data-fetched] {
165 | .audio_row__play_btn {
166 | border: none !important;
167 |
168 | &:after {
169 | display: none !important;
170 | }
171 | }
172 | }
173 |
174 | &[data-fetch-error] {
175 | .audio_row__play_btn {
176 | border: 1px red solid;
177 | border-radius: 50%;
178 |
179 | &:after {
180 | position: absolute;
181 | top: 0;
182 | right: -10px;
183 | display: block;
184 | font-size: 23px;
185 | color: red;
186 | content: '!';
187 | }
188 | }
189 | }
190 |
191 | &:hover {
192 | .bitrate {
193 | display: block;
194 | }
195 | }
196 | }
197 |
198 | .audio_row_current {
199 | .download-link {
200 | top: 5px;
201 | }
202 | }
203 |
204 | .bitrate {
205 | display: none;
206 | position: absolute;
207 | top: -18px;
208 | left: 0;
209 | right: 0;
210 | width: 150px;
211 | margin: 0 auto;
212 | padding: 3px;
213 | text-align: center;
214 | z-index: 100;
215 | color: #2B587A;
216 | background-color: #C0CDDC;
217 | border-radius: 2px;
218 |
219 | &:after {
220 | display: block;
221 | position: absolute;
222 | bottom: -8px;
223 | left: 0;
224 | right: 0;
225 | width: 0;
226 | height: 0;
227 | margin: 0 auto;
228 | border-left: 7px solid transparent;
229 | border-right: 7px solid transparent;
230 | border-top: 8px solid #C0CDDC;
231 | content: '';
232 | }
233 |
234 | > span {
235 | font-size: 10px;
236 | padding-left: 10px;
237 | }
238 | }
239 |
240 | #pad_playlist_panel .bitrate,
241 | #audios_list .bitrate {
242 | right: 72px !important;
243 | }
244 |
245 | #audio.new .audio_add_wrap,
246 | #audio.new .audio_remove_wrap {
247 | margin: 6px 6px 6px 76px !important;
248 | }
249 |
250 | #audio.new .audio_edit_wrap {
251 | margin: 6px 75px 6px 0px !important;
252 | }
253 |
254 | .claimed {
255 | .audio_title_wrap {
256 | &:after {
257 | display: block;
258 | width: 100%;
259 | height: auto;
260 | color: #6383a8;
261 | content: 'Аудиозапись удалена правообладателем';
262 | }
263 | }
264 | }
265 |
266 | embed {
267 | width: 100%;
268 | }
269 |
270 | .idd_wrap.mv_more {
271 | padding: 7px 12px 8px !important;
272 | }
273 |
274 | .arr_div.mv_more {
275 | position: relative;
276 | padding: 7px 0 8px !important;
277 | z-index: 200;
278 |
279 | &:hover {
280 | .idd_popup {
281 | opacity: 1;
282 | }
283 | }
284 |
285 | .idd_selected_value {
286 | text-decoration: none !important;
287 | }
288 |
289 | .idd_popup {
290 | top: 0;
291 | left: -8px;
292 | padding-top: 30px;
293 | background: #eceff5;
294 | z-index: -1;
295 | }
296 |
297 | ul {
298 | list-style: none;
299 | width: auto;
300 | max-width: 785px;
301 | margin: 0;
302 | padding: 0;
303 | background: #fff;
304 |
305 | li {
306 | display: block;
307 | vertical-align: top;
308 | text-align: center;
309 | padding: 12px 8px;
310 |
311 | &:hover {
312 | background: #e5ebf1;
313 | }
314 |
315 | a {
316 | text-decoration: none;
317 | white-space: nowrap;
318 | }
319 | }
320 | }
321 | }
322 |
323 | .cached-status {
324 | position: absolute;
325 | top: 17px;
326 | right: 2px;
327 | font-size: 11px;
328 | color: rgb(43, 88, 122);
329 | min-width: 32px;
330 | padding: 3px;
331 | background-color: rgb(237, 241, 245);
332 | z-index: 100;
333 | }
334 |
335 | #gp_back,
336 | #gp_wrap {
337 | height: 40px !important;
338 | }
339 |
340 | #gp_info {
341 | padding: 6px 8px 5px 0px !important;
342 | }
343 |
344 | #gp_play_btn {
345 | padding: 12px 7px !important;
346 | }
347 |
348 | #gp_back {
349 | border-top-left-radius: 3px !important;
350 | border-bottom-left-radius: 3px !important;
351 | border-top-right-radius: 0 !important;
352 | border-bottom-right-radius: 0 !important;
353 | }
354 |
355 | .top_audio_player {
356 | max-width: 324px !important;
357 | padding-right: 24px !important;
358 | }
359 |
360 | .last-controls {
361 | position: absolute;
362 | top: 0;
363 | right: 5px;
364 | height: 30px;
365 | color: #66819e;
366 | opacity: 0.70;
367 | height: 30px;
368 | border-top-right-radius: 3px;
369 | border-bottom-right-radius: 3px;
370 | transition: opacity 200ms ease-out, background-color 200ms ease-out;
371 | }
372 |
373 | #gp_wrap:hover .last-controls {
374 | opacity: 1;
375 | }
376 |
377 | #scrobble-icon {
378 | width: 20px;
379 | height: 15px;
380 | background-color: white;
381 | border-radius: 50%;
382 | padding-bottom: 5px;
383 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAJCAYAAAA7KqwyAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB50RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNS4xqx9I6wAAAYRJREFUKJFd0U1oznEAB/DP79mzl3aRmkVTdlh2cJBSWoq4UDM5eZ5SiOX222X/XeTG5rC/w/wJKXZ8/koOFCeWHBUHB5SL8ra8tBTtxfNz8KyW7/37HlJKoJYVwyk1p/8sLw1UO7peB8428vhAC/WsGErS9ZWlxW3Vjs4XgWONfOxtSCmpZcVx3EAXPqAPiyml8TuXxq7WsmInHmIdPmIL3gdOVepZsQ838TOwtczj5sAgFkIIV+oTRR/OoCewu8xjP0bxOTEcjo5ffontOFfmcXI1ci0rRlvCF/Abk7iPW4GnjTx+h+rK8uJAtb0T3lmDENxNyTWcrgSDzWQvRjCS/hnMptSMlbZqx7cWp3+tQGM6/sATbGomQ2UeDwQ24jCe4WQIlblQy4oTKaVZzGNPmcc39azYkTgSmEs8xhccKvP4/L96afWFCVxEG76iBwvdy/Prf7X3jmEK3S0T6MUrxAqUeZxuLf2pRX4U2HV75nwq8zgTONiq04sNuBek/WUe5/4CvqiUz462MckAAAAASUVORK5CYII=");
384 | background-position: center;
385 | background-repeat: no-repeat;
386 | opacity: 0.6;
387 | }
388 |
389 | #scrobble-icon.scrobbled {
390 | opacity: 1;
391 | }
392 |
393 | #like-icon,
394 | #like-icon.changed {
395 | opacity: 0.6;
396 | }
397 |
398 | #like-icon {
399 | width: 20px;
400 | height: 15px;
401 | background-color: white;
402 | border-radius: 50%;
403 | margin-top: 4px;
404 | padding-bottom: 5px;
405 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAYAAAAmL5yKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB50RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNS4xqx9I6wAAAVpJREFUKJGlkk0rRFEYx3/PdDNKjC/hA8gCUZPFRFZDFiKRqElTkrFAM4sJZSWT18jLgoWQjShW2CgrH8DOJ/B6595zjsW4YeYO4rf8P//nd06dI8YYAPpnDtNAkhw3iEQ3xtvuAQZmj0Ja6V2gFcgCqY2J9lkAMcbQN70fB+YAiw9uRSQcCMirUvoMaPw0c4DE1mRHRnrSe9XAJVBGITvAMzDoM7OBiKWUShRZBugukgMEgaSllA5/U/qJOstVyv2HIGsppbaB1B8Fi9aL/TxVYgVrDaZZkF9tGQyCHD/aD2kxxtA6vFouATkAIr88+UAr3XuSiT2J95EAWoZXEsYwBQTz75JrmScRGTudjy17+RcBQCS+VAVsAg15jmug62xh6O5zWCAAqOmaDoQqK3aAzvfo1HXc6MXaiJ3f9RV4NMUy50DIcZ36q/VR3+e2/EIPrfUYUFpsGeANMDKDg1etDyIAAAAASUVORK5CYII=");
406 | background-position: center;
407 | background-repeat: no-repeat;
408 | cursor: pointer;
409 | }
410 |
411 | #like-icon.liked,
412 | #like-icon:hover {
413 | opacity: 1;
414 | }
415 |
416 | #main_panel > .bitrate,
417 | #audios_list > #initial_list > .bitrate,
418 | #audios_list > .bitrate,
419 | #pad_playlist > .bitrate,
420 | #pad_playlist_panel > .bitrate,
421 | .pad_audio_table > tbody > tr > .bitrate,
422 | .audio_table > tbody > tr > .bitrate,
423 | #pad_search_list > .bitrate {
424 | display: none;
425 | }
426 |
427 | .audio_rec_wrap.fl_r {
428 | position: absolute;
429 | top: 0;
430 | right: 45px;
431 | }
432 |
--------------------------------------------------------------------------------
/source/views/lastauth.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title Авторизация LastFM
5 | link(href='css/lastauth.css', rel='stylesheet')
6 | body
7 | .content
8 | .wrap
9 | .logo
10 | img(src='images/128x128.png')
11 | h1
12 | span.user вы успешно авторизовали скробблер VK Observer!
13 | script(src='js/libs.js')
14 | script(src='js/lastauth.js')
15 |
--------------------------------------------------------------------------------
/source/views/popup.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset='utf-8')
5 | title Настройки
6 | link(href='css/popup.css', rel='stylesheet')
7 | body
8 | //- .switch.cache
9 | //- .switcher.switcher-cache
10 | //- input(type='checkbox', name='toggle-cache', id='toggle', class='toggle-cache')
11 | //- label(for='toggle-cache')
12 | //- .switcher-label
13 | //- h4 кэширование
14 |
15 | .switch.bitrate
16 | .switcher.switcher-bitrate
17 | input(type='checkbox', name='toggle-scrobble', id='bitrate', class='toggle-bitrate')
18 | label(for='toggle-scrobble')
19 | .switcher-label
20 | h4 битрейт
21 |
22 | .switch.scrobble
23 | .switcher.switcher-scrobble
24 | input(type='checkbox', name='toggle-scrobble', id='scrobble', class='toggle-scrobble')
25 | label(for='toggle-scrobble')
26 | .switcher-label
27 | h4 скробблинг
28 |
29 | a(href='http://vk.com/share.php?image=https://pp.vk.me/c616819/v616819030/1c5d2/ZfAqPNmRN6s.jpg&title=VK Observer&description=загрузка музыки и видео, скробблинг Last.fm&url=https://vk.com/vkobserverchrome', target='_blank', class="social-likes__widget social-likes_light social-likes__widget_vkontakte")
30 | span.social-likes__button.social-likes__button_vkontakte
31 | span.social-likes__icon.social-likes__icon_vkontakte
32 | | Прокачай друга
33 |
34 | script(src='js/popup.js')
35 |
--------------------------------------------------------------------------------
/vkobserver_2.3.5.crx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glebcha/vk-observer/430713ad3c94b8bfd8a16c1892d5cf8ded3e69b8/vkobserver_2.3.5.crx
--------------------------------------------------------------------------------