├── .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 | ![Screenshot0](http://aviaps.ru/images/vkobserver-last.png) 42 | * * * 43 | ![Screenshot1](http://i.imgur.com/vY4Kwrg.png) 44 | * * * 45 | ![Screenshot2](http://i.imgur.com/zwoPh97.png) 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('
'); 78 | //API accept only UTF-8 and that's why non-latin chars will cause error in default js lib 79 | 80 | /* Write POST parameters as input fields. */ 81 | for(var param in params){ 82 | doc.write(''); 83 | } 84 | 85 | /* Write automatic form submission code. */ 86 | 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 --------------------------------------------------------------------------------