// ==UserScript==
// @name Bgm.tv auto tracker
// @namespace https://trim21.me/
// @description auto tracker your bangumi progress
// @version 0.5.0
// @author Trim21
// @match https://www.bilibili.com/bangumi/play/*
// @match http*://www.iqiyi.com/*
// @match https://bangumi-auto-tracker.trim21.cn/oauth_callback*
// @match https://bangumi-auto-tracker.trim21.cn/userscript/options*
// @require https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js
// @require https://cdn.bootcss.com/axios/0.18.0/axios.js
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect localhost
// @connect api.bgm.tv
// @connect bangumi-auto-tracker.trim21.cn
// @run-at document-end
// ==/UserScript==
(function () {
'use strict'
/* eslint-disable no-undef, camelcase */
let tm_unsafeWindow = unsafeWindow
let tm_xmlHttpRequest = GM_xmlhttpRequest
let tm_setValue = GM_setValue
let tm_getValue = GM_getValue
let tm_openInTab = GM_openInTab
let tm_addStyle = GM_addStyle
let $ = window.$
let website
/* eslint-enable no-undef, camelcase */
let auth
let bangumiData = {}
console.log('hello world')
function notify (message) {
let now = new Date()
$('#bgm_tv_tracker_notification')
.prepend(`<hr><p>${now.getHours()}:${now.getMinutes()}:${now.getSeconds()} ${message}</p>`)
}
const parseHeader = function (lines) {
let headers = {}
for (let line of lines.trim().split('\r')) {
line = line.trim()
if (line) {
Object.assign(headers, parseHeaderLine(line))
}
}
return headers
}
const parseHeaderLine = function (line) {
let headers = {}
let headerExp = /^([^: \t]+):[ \t]*((?:.*[^ \t])|)/
let match = headerExp.exec(line)
let k = match && match[1]
k = k.toLowerCase()
headers[k] = match[2]
return headers
}
const NORMAL_ONLOAD = (resolve, reject) => (response) => {
response.headers = parseHeader(response.responseHeaders)
if (response.status < 300) {
if (response.headers['content-type'].startsWith('application/json')) {
response.data = JSON.parse(response.responseText)
}
resolve(response)
} else {
console.log(response)
if (response.headers['content-type'].startsWith('application/json')) {
response.data = JSON.parse(response.responseText)
}
reject({ response })
}
}
const requests = {
get (url, headers = {}) {
// headers.cookie = ''
return new Promise((resolve, reject) => {
tm_xmlHttpRequest({
method: 'GET',
url,
headers,
onload: NORMAL_ONLOAD(resolve, reject)
})
})
},
post (url, data = {}, headers = {}) {
if (data !== null && typeof data === 'object') {
data = JSON.stringify(data)
headers['content-Type'] = 'application/json'
}
// headers.cookie = ''
return new Promise((resolve, reject) => {
tm_xmlHttpRequest({
method: 'POST',
data,
url,
headers,
onload: NORMAL_ONLOAD(resolve, reject)
})
})
}
}
const bgmApi = {
get (url, headers = {}) {
Object.assign(headers, { 'User-Agent': 'Bgm.tv auto tracker' })
return new Promise((resolve, reject) => {
requests.get(url, headers).then(
response => {
if (response.data.code && response.data.code >= 300) {
let error = { response }
reject(error)
} else {
resolve(response)
}
},
error => reject(error)
)
})
},
post (url, data = {}, headers = {}) {
Object.assign(headers, { 'User-Agent': 'Bgm.tv auto tracker' })
// headers.cookie = ''
return new Promise((resolve, reject) => {
requests.post(url, data, headers).then(
response => {
if (response.data.code && response.data.code >= 300) {
let error = { response }
reject(error)
} else {
resolve(response)
}
},
error => reject(error)
)
})
}
}
const VARS = {
apiServerURL: 'https://bangumi-auto-tracker.trim21.cn',
callBackUrl: 'https://bangumi-auto-tracker.trim21.cn/oauth_callback',
apiBgmUrl: 'https://api.bgm.tv',
authURL: ''
}
VARS.authURL = 'https://bgm.tv/oauth/authorize?client_id=bgm2775b2797b4d958b&response_type=code&redirect_uri=' + VARS.callBackUrl
if (window.TM_ENV === 'dev') {
VARS.apiServerURL = 'http://localhost:6001'
console.log('dev')
}
function getEps (subjectID) {
return new Promise(
(resolve, reject) => {
let eps = tm_getValue(`eps_${subjectID}`, false)
if (!eps) {
bgmApi.get(`${VARS.apiBgmUrl}/subject/${subjectID}/ep`).then(
(response) => {
response.data.time = Number(new Date().getTime() / 1000)
tm_setValue(`eps_${subjectID}`, JSON.stringify(response.data))
resolve(response.data)
},
(error) => {
reject(error)
notify('get bgm eps error', 2)
}
)
} else {
eps = JSON.parse(eps)
if (Number(new Date().getTime() / 1000) - eps.time > 60 * 60 * 2) {
requests.get(`${VARS.apiBgmUrl}/subject/${subjectID}/ep`).then(
(response) => {
response.data.time = Number(new Date().getTime() / 1000)
tm_setValue(`eps_${subjectID}`, JSON.stringify(response.data))
resolve(response.data)
},
(error) => {
reject(error)
notify('get bgm eps error', 2)
}
)
} else {
resolve(eps)
}
}
}
)
}
let collection = tm_getValue('collection', false) // @type {Array}
if (!collection) {
collection = {}
} else {
collection = JSON.parse(collection)
}
function collectSubject (subjectID) {
if (!collection[subjectID]) {
requests.post(`${VARS.apiBgmUrl}/collection/${subjectID}/update`, 'status=do',
{
'content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer ' + auth.access_token
}).then(
response => {
if (response.data.code === 401) {
notify(response.data.error)
} else {
notify('add this bangumi to your collection', 2)
collection[subjectID] = true
tm_setValue('collection', JSON.stringify(collection))
}
},
error => notify(error.response.data.error_description)
)
}
}
function watchEpisode (message) {
collectSubject(message.subject_id)
getEps(message.subject_id).then(
(data) => {
console.log(message.episode)
let ep = data.eps.filter(function (val) {
return val.sort === parseInt(message.episode)
})
console.log(ep)
ep = ep[0].id
requests.post(`${VARS.apiBgmUrl}/ep/${ep}/status/watched`,
null, { 'Authorization': 'Bearer ' + auth.access_token }).then(
() => notify(`mark your status successfully`.toString(), 2),
error => notify(JSON.stringify(error.response.data))
)
},
error => notify(JSON.stringify(error.response.data))
).catch(function (err) {
notify(err.toString(), 2)
})
}
// auth
if (tm_unsafeWindow.location.href.startsWith(VARS.callBackUrl)) {
if (tm_unsafeWindow.data) {
tm_setValue('auth', JSON.stringify(tm_unsafeWindow.data))
let child = tm_unsafeWindow.document.createElement('h1')
child.innerText = '成功授权 请关闭网页'
tm_unsafeWindow.document.body.appendChild(child)
}
} else {
auth = tm_getValue('auth', false)
if (auth) {
auth = JSON.parse(auth)
} else {
let r = tm_unsafeWindow.alert('you need to auth bgm.tv_auto_tracker first')
if (r) {
tm_openInTab(VARS.authURL, { active: true })
}
}
}
// inject bilibili
if (tm_unsafeWindow.location.href.startsWith('https://www.bilibili.com/bangumi/play/')) {
console.log('inject bilibili')
website = 'bilibili'
const dealWithSubjectID = function (subjectID) {
bangumiData.subjectID = subjectID
$('#bgm_tv_tracker_link').html(`<a href="http://bgm.tv/subject/${subjectID}" target="_blank" rel="noopener noreferrer">subject/${subjectID}</a>`)
$('#bgm_tv_tracker_mark_watched').click(
() => {
let ep = $('#bgm_tv_tracker_episode').html()
collectSubject(subjectID)
getEps(subjectID).then(data => {
let eps = data.eps.findIndex(function (element) {
return element.sort === parseInt(ep)
}) + 1
bgmApi.post(`https://api.bgm.tv/subject/${subjectID}/update/watched_eps?watched_eps=${eps}`,
`watched_eps=${eps}`, {
'content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer ' + auth.access_token
})
.then(
(response) => {
if (response.data.code === 202) {
notify('mark status successful')
} else {
notify('error: ' + JSON.stringify(response.data))
}
},
error => notify('error: ' + JSON.stringify(error))
)
})
}
)
$('#bgm_tv_tracker_mark_watch').click(
() => {
watchEpisode({
subject_id: subjectID,
'type': 'watch_episode',
'website': 'bilibili',
'bangumi_id': $('#bgm_tv_tracker').data('id'),
'title': $('#bgm_tv_tracker_title').html(),
episode: $('#bgm_tv_tracker_episode').html()
})
}
)
}
// noinspection JSAnnotator
const injectBilibili = function () {
const status = tm_unsafeWindow.__INITIAL_STATE__
const episode = status.epInfo.index
const bangumiID = status.mediaInfo.season_id
bangumiData.bangumiID = status.mediaInfo.season_id
bangumiData.episode = episode
$('#bangumi_detail > div > div.info-right > div.info-title.clearfix > div.func-module.clearfix')
.prepend('<div id="bgm_tv_tracker" class="disable" data-id=""><div class="bgm_tv_tracker_btn bgm_tv_tracker bgm_tv_tracker_radius">bgm.tv</div><div class="bgm_tv_tracker_info"><div class="not_found"></div><br><div><p>你正在看: <span id="bgm_tv_tracker_title"></span></p><p>第 <span id="bgm_tv_tracker_episode">{episode}</span>集</p></div><br><div id="bgm_tv_tracker_link"></div><br><button class="bgm_tv_tracker_radius" id="bgm_tv_tracker_mark_watch">标记本集为看过</button> <button class="bgm_tv_tracker_radius" id="bgm_tv_tracker_mark_watched">看到本集</button><br><br><a href="https://github.com/Trim21/bilibili-bangumi-tv-auto-tracker/issues" target="_blank" rel="noopener noreferrer">报告问题</a><br><div id="bgm_tv_tracker_notification"></div></div></div>')
tm_addStyle('#bgm_tv_tracker{display:inline-block;position:relative;float:left;margin-right:20px;user-select:none}.bgm_tv_tracker_radius{border-radius:4px;border:1px solid #e5e9ef}.bgm_tv_tracker_btn.bgm_tv_tracker{color:#6d757a;float:left;cursor:pointer;font-size:14px;height:28px;line-height:28px;text-align:center;width:80px!important;transition:all .1s ease-in}#bangumi_detail .bangumi-info.clearfix .info-right .info-title.clearfix a h2{width:380px}@media screen and (max-width:1400px){.arc-toolbar .block{padding:0 12px;margin-left:-12px}.video-toolbar-module .btn-item{padding:0 0 0 60px!important;margin-left:-12px}#bangumi_detail .bangumi-info.clearfix .info-right .info-title.clearfix a h2{width:200px!important}}#bgm_tv_tracker.disable .bgm_tv_tracker_info{display:none}.bgm_tv_tracker_info{padding:8px;margin-top:5px;background:#fff;border-radius:0 0 4px 4px;border:1px solid #e5e9ef;box-shadow:rgba(0,0,0,.16) 0 2px 4px;cursor:default;height:auto;left:-1px;line-height:normal;opacity:0;pointer-events:none;position:absolute;text-align:left;top:70px;white-space:normal;width:300px;z-index:1000}.bgm_tv_tracker_info *{max-width:100%}#bgm_tv_tracker .bgm_tv_tracker_info{opacity:1;pointer-events:auto;top:100%}.bgm_tv_tracker_info button{padding:4px 6px;line-height:14px;display:inline-block;margin:4px;border:2px solid #fff}.bgm_tv_tracker_info button:active{background:#fff}.bgm_tv_tracker_info button:hover{border:2px solid #99bdf7}')
let info = $('.bgm_tv_tracker_info')
$('.bgm_tv_tracker_btn.bgm_tv_tracker').click(() => {
info.toggle('fast')
}).hover(function () {
$(this).css('background-color', '#00A1D6')
$(this).css('color', 'white')
}, function () {
$(this).css('background-color', 'white')
$(this).css('color', 'black')
})
$('#bgm_tv_tracker_episode').html(episode)
$('#bgm_tv_tracker').data('id', bangumiID)
$('#bgm_tv_tracker_title').html(status.mediaInfo.title)
requests.get(`${VARS.apiServerURL}/query/bilibili?bangumi_id=${bangumiID}`).then(
(response) => {
let subjectID = response.data.bangumi_id || response.data.subject_id
dealWithSubjectID(subjectID)
},
(err) => {
if (err.response.status === 404) {
// $('.bgm_tv_tracker_info').html('没找到你在看的番剧')
// const subjectID = tm_unsafeWindow.prompt('Bgm.tv auto tracker 没找到你在看的番剧 手动指定subject id')
let notFound = $('.bgm_tv_tracker_info .not_found')
.html('<label><input type="text" class="subject"> <button class="notfound">submit subject id</button></label>')
// $('.bgm_tv_tracker_info .not_found')
$('.bgm_tv_tracker_info .not_found button').click(
() => {
let subjectID = $('.bgm_tv_tracker_info .not_found input').val()
if (subjectID) {
notFound.hide()
requests.get(`${VARS.apiServerURL}/api/v0.1/missingBilibili?bangumi_id=${bangumiID}&subject_id=${subjectID}`)
dealWithSubjectID(subjectID)
}
})
}
}
)
}
injectBilibili()
let INNER_EPISODE = tm_unsafeWindow.__INITIAL_STATE__.epInfo.index
// noinspection JSAnnotator
const onHrefChange = function () {
const status = tm_unsafeWindow.__INITIAL_STATE__
const episode = status.epInfo.index
bangumiData.bangumiID = status.mediaInfo.season_id
bangumiData.episode = episode
$('#bgm_tv_tracker_episode').html(episode)
}
// noinspection JSAnnotator
const detectHrefChange = function () {
console.log('check href')
if (INNER_EPISODE !== tm_unsafeWindow.__INITIAL_STATE__.epInfo.index) {
onHrefChange()
INNER_EPISODE = tm_unsafeWindow.__INITIAL_STATE__.epInfo.index
}
}
setInterval(detectHrefChange, 10 * 1000)
setTimeout(detectHrefChange, 5000)
}
// inject iqiyi
if (tm_unsafeWindow.location.hostname === 'www.iqiyi.com') {
console.log(tm_unsafeWindow.Q.PageInfo.playPageInfo.categoryName)
website = 'iqiyi'
let videoID
let title = tm_unsafeWindow.document.title
const injectIqiyi = function () {
console.log('inject iqiyi just for collecting animation data now')
let bangumiName = $('#datainfo-navlist > a:nth-child(3)').html()
$('#jujiPlayWrap > div:nth-child(2) > div > div > div.funcRight.funcRight1014')
.prepend('<div id="bgm_tv_tracker" class="disable" data-id=""><div class="bgm_tv_tracker_btn bgm_tv_tracker bgm_tv_tracker_radius">bgm.tv</div><div class="bgm_tv_tracker_info"><div class="not_found"></div><br><div><p>你正在看: <span id="bgm_tv_tracker_title"></span></p><p>第 <span id="bgm_tv_tracker_episode">{episode}</span>集</p></div><br><div id="bgm_tv_tracker_link"></div><br><button class="bgm_tv_tracker_radius" id="bgm_tv_tracker_mark_watch">标记本集为看过</button> <button class="bgm_tv_tracker_radius" id="bgm_tv_tracker_mark_watched">看到本集</button><br><br><a href="https://github.com/Trim21/bilibili-bangumi-tv-auto-tracker/issues" target="_blank" rel="noopener noreferrer">报告问题</a><br><div id="bgm_tv_tracker_notification"></div></div></div>')
tm_addStyle('#bgm_tv_tracker{margin-left:15px;padding-left:16px;position:relative;font-size:15px;float:left;cursor:pointer;display:inline;user-select:none}.bgm_tv_tracker_btn.bgm_tv_tracker{float:left;cursor:pointer;font-size:14px;height:28px;line-height:18px;text-align:center;width:80px!important;transition:all .1s ease-in}#bgm_tv_tracker.disable .bgm_tv_tracker_info{display:none}.bgm_tv_tracker_info{padding:8px;margin-top:5px;background:#fff;border:1px solid #e5e9ef;cursor:default;height:auto;left:-1px;line-height:normal;opacity:0;pointer-events:none;position:absolute;text-align:left;top:70px;white-space:normal;width:300px;z-index:1000}.bgm_tv_tracker_info *{max-width:100%}#bgm_tv_tracker .bgm_tv_tracker_info{opacity:1;pointer-events:auto;top:100%}.bgm_tv_tracker_info button{padding:4px 6px;line-height:14px;display:inline-block;margin:4px}')
let info = $('.bgm_tv_tracker_info')
$('.bgm_tv_tracker_btn.bgm_tv_tracker').click(() => {
info.toggle('fast')
}).hover(function () {
$(this).css('color', '#5aa700')
}, function () {
$(this).css('color', '#959799')
})
console.log(bangumiName)
title = tm_unsafeWindow.document.title
requests.post(`${VARS.apiServerURL}/api/v0.1/parser/title`, {
title: bangumiName,
title_with_episode: title
}).then(
(response) => {
$('#bgm_tv_tracker_title').html(bangumiName)
$('#bgm_tv_tracker_episode').html(response.data.episode)
$('#bgm_tv_tracker').data('id', response.data.bangumi_id)
let subjectID = response.data.bangumi_id || response.data.subject_id
bangumiData.subjectID = subjectID
$('#bgm_tv_tracker_link').html(`<a href="http://bgm.tv/subject/${subjectID}" target="_blank" rel="noopener noreferrer">subject/${subjectID}</a>`)
$('#bgm_tv_tracker_mark_watched').click(
() => {
let ep = $('#bgm_tv_tracker_episode').html()
collectSubject(subjectID)
getEps(subjectID).then(data => {
let eps = data.eps.findIndex(function (element) {
return element.sort === parseInt(ep)
}) + 1
bgmApi.post(`https://api.bgm.tv/subject/${subjectID}/update/watched_eps?watched_eps=${eps}`,
`watched_eps=${eps}`, {
'content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer ' + auth.access_token
})
.then(
(response) => {
if (response.data.code === 202) {
notify('mark status successful')
} else {
notify('error: ' + JSON.stringify(response.data))
}
},
error => notify('error: ' + JSON.stringify(error))
)
})
}
)
$('#bgm_tv_tracker_mark_watch').click(
() => {
watchEpisode({
subject_id: subjectID,
'type': 'watch_episode',
'website': 'bilibili',
'bangumi_id': $('#bgm_tv_tracker').data('id'),
'title': $('#bgm_tv_tracker_title').html(),
episode: $('#bgm_tv_tracker_episode').html()
})
}
)
},
(err) => {
if (err.response.status === 404) {
$('.bgm_tv_tracker_info').html('没找到你在看的番剧')
}
}
)
}
// noinspection JSAnnotator
const onHrefChange = function () {
console.log('hash change')
if (!(videoID !== tm_unsafeWindow._player._videoid &&
title !== tm_unsafeWindow.document.title)) {
console.log('video not change')
setTimeout(onHrefChange, 500)
}
videoID = tm_unsafeWindow._player._videoid
title = tm_unsafeWindow.document.title
}
if (tm_unsafeWindow.Q.PageInfo.playPageInfo.categoryName === '动漫') {
setTimeout(injectIqiyi, 1000)
tm_unsafeWindow.addEventListener('hashchange', function () {
setTimeout(onHrefChange, 500)
}, false)
}
}
})()