├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── requirements.txt ├── scripts ├── build.sh ├── makevardir.sh └── makevirtualenv.sh └── src ├── app ├── app.ico ├── index.html ├── main.js ├── manual.md ├── messenger.js ├── package-lock.json ├── package.json ├── render.js └── window.js └── service ├── db.py ├── handlers ├── __init__.py ├── dashboard.py ├── exports.py ├── handlers.py ├── my │ ├── __init__.py │ └── my.py ├── search.py └── settings │ ├── __init__.py │ ├── accounts.py │ └── settings.py ├── main.py ├── server.py ├── service.ico ├── setting.py ├── static ├── 404.jpg ├── bulma.min.css ├── fontawesome-all.min.js ├── logo.png ├── note.css ├── photo_album.png ├── star_matrix.png └── theme.css ├── tasks ├── __init__.py ├── exceptions.py └── tasks.py ├── tests.py ├── uimodules.py ├── urls.py ├── version.py ├── views ├── album.html ├── book.html ├── broadcast.html ├── dashboard.html ├── errors │ └── 404.html ├── exports.html ├── index.html ├── manual.html ├── modules │ ├── account.html │ ├── book.html │ ├── movie.html │ ├── music.html │ ├── note.html │ ├── recommend.html │ └── user.html ├── movie.html ├── music.html ├── my │ ├── _broadcast.html │ ├── _menu.html │ ├── blocklist.html │ ├── blocklist_historical.html │ ├── book.html │ ├── book_historical.html │ ├── broadcast.html │ ├── favorite │ │ ├── note.html │ │ └── photo.html │ ├── followers.html │ ├── followers_historical.html │ ├── following.html │ ├── following_historical.html │ ├── movie.html │ ├── music.html │ ├── music_historical.html │ ├── my.html │ ├── note.html │ └── photo.html ├── note.html ├── photo.html ├── search.html ├── settings │ ├── _menu.html │ ├── accounts │ │ ├── index.html │ │ └── login.html │ ├── general.html │ ├── network.html │ └── settings.html ├── themes │ ├── base.html │ ├── main.html │ └── paginator.html └── user.html └── worker.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Byte-compiled / optimized / DLL files 61 | __pycache__/ 62 | *.py[cod] 63 | *$py.class 64 | 65 | # C extensions 66 | *.so 67 | 68 | # Distribution / packaging 69 | .Python 70 | build/ 71 | env/ 72 | develop-eggs/ 73 | dist/ 74 | downloads/ 75 | eggs/ 76 | .eggs/ 77 | lib/ 78 | lib64/ 79 | parts/ 80 | sdist/ 81 | var/ 82 | wheels/ 83 | *.egg-info/ 84 | .installed.cfg 85 | *.egg 86 | 87 | # PyInstaller 88 | # Usually these files are written by a python script from a template 89 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 90 | *.manifest 91 | *.spec 92 | 93 | # Installer logs 94 | pip-log.txt 95 | pip-delete-this-directory.txt 96 | 97 | # Unit test / coverage reports 98 | htmlcov/ 99 | .tox/ 100 | .coverage 101 | .coverage.* 102 | .cache 103 | nosetests.xml 104 | coverage.xml 105 | *.cover 106 | .hypothesis/ 107 | 108 | # Translations 109 | *.mo 110 | *.pot 111 | 112 | # Django stuff: 113 | *.log 114 | local_settings.py 115 | 116 | # Flask stuff: 117 | instance/ 118 | .webassets-cache 119 | 120 | # Scrapy stuff: 121 | .scrapy 122 | 123 | # Sphinx documentation 124 | docs/_build/ 125 | 126 | # PyBuilder 127 | target/ 128 | 129 | # Jupyter Notebook 130 | .ipynb_checkpoints 131 | 132 | # pyenv 133 | .python-version 134 | 135 | # celery beat schedule file 136 | celerybeat-schedule 137 | 138 | # SageMath parsed files 139 | *.sage.py 140 | 141 | # dotenv 142 | .env 143 | 144 | # virtualenv 145 | .venv 146 | venv/ 147 | ENV/ 148 | 149 | # Spyder project settings 150 | .spyderproject 151 | .spyproject 152 | 153 | # Rope project settings 154 | .ropeproject 155 | 156 | # mkdocs documentation 157 | /site 158 | 159 | # mypy 160 | .mypy_cache/ 161 | 162 | # jshint 163 | .jshintrc 164 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | 5 | { 6 | "name": "Debug Main Process", 7 | "type": "node", 8 | "request": "launch", 9 | "cwd": "${workspaceRoot}", 10 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 11 | "windows": { 12 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 13 | }, 14 | "program": "${workspaceRoot}/src/app/main.js", 15 | "protocol": "inspector" 16 | }, 17 | { 18 | "name": "Debug Renderer Process", 19 | "type": "chrome", 20 | "request": "launch", 21 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 22 | "windows": { 23 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 24 | }, 25 | "runtimeArgs": [ 26 | "${workspaceRoot}/src/app/main.js", 27 | "--remote-debugging-port=9222" 28 | ], 29 | "webRoot": "${workspaceRoot}" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "${workspaceFolder}\\.venv\\Scripts\\python.exe", 3 | "python.linting.pylintEnabled": true, 4 | "python.linting.enabled": true, 5 | "git.ignoreLimitWarning": true 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "test", 9 | "problemMatcher": [] 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 tabris17 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 此项目已废弃。新版豆瓣备份工具,请移步 https://github.com/doufen-org/tofu 2 | 3 | # 豆坟 4 | 5 | 用来备份豆瓣帐号的软件。目前支持备份关注、黑名单、书影音、广播。 6 | 7 | 开发环境要求: 8 | 9 | - VSCode 10 | - Python 3.6 11 | - virtualenv 15.2 12 | - Nodejs 8.9 13 | - npm 5.8 14 | - git 2.16 15 | 16 | ## 开始 17 | 18 | > npm config set script-shell "C:\\Program Files\\Git\\usr\\bin\\bash.exe" 19 | > npm i 20 | 21 | 如果安装 peewee 组件提示『找不到sqlite3.h』错误,尝试使用如下方法安装: 22 | 23 | > set NO_SQLITE=1 24 | > pip install peewee 25 | 26 | ## 命令 27 | 28 | 调式 app: 29 | 30 | > npm run app 31 | 32 | 调试 service: 33 | 34 | > npm run service 35 | 36 | 打包 app: 37 | 38 | > npm run build:app 39 | 40 | 打包 service: 41 | 42 | > npm run build:service 43 | 44 | ## Linux 和 MacOS 45 | 46 | 在 Unix-like 的系统下,Virtualenv 的激活命令为: 47 | 48 | > .venv/bin/activate 49 | 50 | 直接运行 npm i 会提示“./.venv/Scripts/activate: No such file or directory”错误。请修改 package.json 中相应的命令。 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doufen-platform", 3 | "version": "0.5.1", 4 | "description": "", 5 | "scripts": { 6 | "start": "echo please use '\"'npm run app'\"' and '\"'npm run service'\"'", 7 | "postinstall": "virtualenv .venv && source ./.venv/Scripts/activate && pip install -r ./requirements.txt && mkdir var && mkdir var/data && mkdir var/log", 8 | "app": "electron ./src/app --debug", 9 | "service": "source ./.venv/Scripts/activate && python ./src/service/main.py --debug", 10 | "build:service": "source ./.venv/Scripts/activate && source ./scripts/build.sh", 11 | "build:app": "electron-builder --win --x64 --project ./src/app" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/tabris17/doufen.git" 16 | }, 17 | "author": "tabris17", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/tabris17/doufen/issues" 21 | }, 22 | "homepage": "https://github.com/tabris17/doufen#readme", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "electron": "^1.8.4", 26 | "electron-builder": "^20.8.0" 27 | } 28 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado==5.0.1 2 | pyinstaller==3.3.1 3 | peewee==3.3.2 4 | requests==2.18.4 5 | pyquery==1.4.0 6 | openpyxl==2.5.3 -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | pyinstaller -y -w -F -n service -i src/service/service.ico --add-data="src/service/views;views" --add-data="src/service/static;static" src/service/main.py -------------------------------------------------------------------------------- /scripts/makevardir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir var && mkdir var/data && mkdir var/log -------------------------------------------------------------------------------- /scripts/makevirtualenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | virtualenv .venv && source ./.venv/Scripts/activate && pip install -r ./requirements.txt -------------------------------------------------------------------------------- /src/app/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabris17/doufen/5de4212f50cc2b31423db0a0ed86ca82bed05213/src/app/app.ico -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 65 | 66 | 67 | 68 |
69 |

70 | 正在启动服务... 71 |

72 | 豆坟 73 | 76 |
77 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/app/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 主进程代码入口 3 | */ 4 | const path = require('path') 5 | const url = require('url') 6 | const { ArgumentParser } = require('argparse') 7 | const { app, dialog, ipcMain, Notification } = require('electron') 8 | const { splashScreen, createMainWindow, createTray, getMainWindow } = require('./window') 9 | const childProcess = require('child_process') 10 | const electronReferer = require('electron-referer') 11 | const Messenger = require('./messenger') 12 | 13 | 14 | const DEFAULT_SERVICE_PORT = 8398 15 | const DEFAULT_SERVICE_HOST = '127.0.0.1' 16 | const MESSENGER_RECONNECT_TIMES = 10 17 | const MESSENGER_RECONNECT_INTERVAL = 5000 18 | const MAX_LOG_LINES = 1000 19 | 20 | 21 | /** 22 | * 解析命令行参数 23 | * 24 | * @param {Array} args 25 | */ 26 | function parseArgs(args) { 27 | const package = require('./package.json') 28 | let argsParser = ArgumentParser({ 29 | version: package.version, 30 | prog: package.commandName, 31 | addHelp: true 32 | }) 33 | argsParser.addArgument( 34 | ['-d', '--debug'], { 35 | action: 'storeTrue', 36 | help: 'Set debug mode on.', 37 | defaultValue: false, 38 | required: false 39 | } 40 | ) 41 | argsParser.addArgument( 42 | ['-p', '--port'], { 43 | action: 'store', 44 | dest: 'port', 45 | metavar: 'port', 46 | type: 'int', 47 | help: 'Specify the port of service.', 48 | defaultValue: DEFAULT_SERVICE_PORT, 49 | required: false 50 | } 51 | ) 52 | return argsParser.parseArgs(args) 53 | } 54 | 55 | 56 | /** 57 | * 确保程序单例运行 58 | */ 59 | function ensureSingleton() { 60 | const isDuplicateInstance = app.makeSingleInstance((commandLine, workingDirectory) => { 61 | let win = getMainWindow() 62 | if (win) { 63 | if (!win.isVisible()) win.show() 64 | else if (win.isMinimized()) win.restore() 65 | win.focus() 66 | } 67 | }) 68 | if (isDuplicateInstance) { 69 | app.quit() 70 | } 71 | 72 | return isDuplicateInstance 73 | } 74 | 75 | 76 | let logger = { 77 | lines: new Array(MAX_LOG_LINES), 78 | cursor: 0, 79 | append(line) { 80 | this.lines[this.cursor] = line 81 | return this.cursor = (this.cursor + 1) % MAX_LOG_LINES 82 | }, 83 | all() { 84 | let partA = this.lines.slice(this.cursor).join("\n") 85 | let partB = this.lines.slice(0, this.cursor).join("\n") 86 | return (partA + partB).trim() 87 | }, 88 | restore() { 89 | this.cursor = 0 90 | this.lines = new Array(MAX_LOG_LINES) 91 | } 92 | } 93 | 94 | global.sharedData = { 95 | logger: logger 96 | } 97 | 98 | 99 | /** 100 | * 程序入口主函数 101 | * 102 | * @param {Array} args 103 | */ 104 | function main(args) { 105 | if (ensureSingleton()) { 106 | return 107 | } 108 | 109 | let parsedArgs = parseArgs(args) 110 | 111 | global.debugMode = parsedArgs.debug 112 | if (debugMode) { 113 | console.debug = console.log 114 | } else { 115 | console.debug = (...args) => {} 116 | childProcess.spawn('service', ['-q']) 117 | } 118 | 119 | let serviceUrl = url.format({ 120 | port: parsedArgs.port, 121 | pathname: '/', 122 | protocol: 'http:', 123 | hostname: DEFAULT_SERVICE_HOST 124 | }) 125 | 126 | let messenger = new Messenger(url.format({ 127 | port: parsedArgs.port, 128 | pathname: '/notify', 129 | protocol: 'ws:', 130 | hostname: DEFAULT_SERVICE_HOST 131 | }), 100, times=MESSENGER_RECONNECT_TIMES) 132 | 133 | messenger.on('fail', () => { 134 | dialog.showMessageBox({ 135 | type: 'error', 136 | buttons: ['确定(&O)'], 137 | noLink: true, 138 | title: '错误', 139 | normalizeAccessKeys: true, 140 | message: '服务没有响应。请重新启动程序。' 141 | }, () => { 142 | app.exit() 143 | }) 144 | }) 145 | 146 | app.on('ready', () => { 147 | let splash = splashScreen() 148 | let win = createMainWindow() 149 | 150 | const handleMessengerClose = () => { 151 | messenger.once('close', () => { 152 | let notification = new Notification({ 153 | title: '错误', 154 | body: '服务连接中断。正在尝试重新连接...' 155 | }) 156 | notification.show() 157 | 158 | messenger.connect(MESSENGER_RECONNECT_INTERVAL, 3) 159 | messenger.once('open', () => { 160 | let notification = new Notification({ 161 | title: '消息', 162 | body: '服务重新连接成功!' 163 | }) 164 | notification.show() 165 | 166 | handleMessengerClose() 167 | }) 168 | }) 169 | } 170 | 171 | const bootstrap = () => { 172 | console.debug('service started') 173 | 174 | let splashWebContents = splash.webContents 175 | splashWebContents.send('ready-to-connect') 176 | 177 | win.loadURL(serviceUrl) 178 | win.once('ready-to-show', () => { 179 | console.debug('main window rendered') 180 | 181 | splashWebContents.send('ready-to-show') 182 | splash.once('closed', () => { 183 | win.show() 184 | createTray() 185 | }) 186 | }) 187 | 188 | handleMessengerClose() 189 | } 190 | 191 | console.debug('waiting for service to be started') 192 | if (messenger.isConnected) { 193 | bootstrap() 194 | } else { 195 | messenger.once('open', () => { 196 | bootstrap() 197 | }) 198 | } 199 | 200 | messenger.on('message', (data) => { 201 | switch (data.sender) { 202 | case 'logger': 203 | logger.append(data.message) 204 | win.webContents.send('logger-update') 205 | break 206 | case 'worker': 207 | let noticeTitle, noticeBody 208 | switch (data.event) { 209 | case 'error': 210 | noticeTitle = '通知' 211 | noticeBody = `工作进程"${data.src}"发生错误: ${data.message}` 212 | break 213 | case 'done': 214 | noticeTitle = '通知' 215 | noticeBody = `工作进程"${data.src}"执行完毕` 216 | win.webContents.send('worker-status-change') 217 | break 218 | case 'ready': 219 | noticeTitle = '通知' 220 | noticeBody = `工作进程"${data.src}"已启动` 221 | win.webContents.send('worker-status-change') 222 | break 223 | case 'working': 224 | noticeTitle = '通知' 225 | noticeBody = `工作进程"${data.src}"开始执行任务"${data.target}"` 226 | win.webContents.send('worker-status-change') 227 | break 228 | } 229 | (new Notification({ 230 | title: noticeTitle, 231 | body: noticeBody 232 | })).show() 233 | break 234 | } 235 | }) 236 | 237 | }) 238 | 239 | app.on('activate', () => { 240 | if (!getMainWindow()) { 241 | let win = createMainWindow() 242 | win.loadURL(serviceUrl).once('ready-to-show', () => { 243 | win.show() 244 | }) 245 | } 246 | }) 247 | 248 | app.on('window-all-closed', () => { 249 | if (process.platform !== 'darwin') { 250 | app.quit() 251 | } 252 | }) 253 | 254 | app.on('will-quit', () => { 255 | console.debug('app will quit') 256 | messenger.close() 257 | }) 258 | 259 | electronReferer('https://www.douban.com/') 260 | } 261 | 262 | let mainArgs = [] 263 | 264 | if (process.argv[1] && [__filename, __dirname].indexOf(path.resolve(process.argv[1])) > -1) { 265 | mainArgs = process.argv.slice(2) 266 | } else if (process.argv) { 267 | mainArgs = process.argv.slice(1) 268 | } 269 | 270 | main(mainArgs) -------------------------------------------------------------------------------- /src/app/manual.md: -------------------------------------------------------------------------------- 1 | # 在豆瓣坟头蹦迪,简称『豆坟』 2 | 3 | **豆坟**是一个用来备份你豆瓣账号数据的软件。软件可以抓取你的豆瓣数据,并在本地进行脱机浏览。 4 | 5 | 请先在“控制台”的“账号管理”里添加你的豆瓣账号,然后在“控制台”的“后台服务”里点击“开始”按钮。程序会自动爬取你的豆瓣数据,并保存到本地数据库中。 6 | 7 | ## 安全警告 8 | 9 | 为了保护你的豆瓣账号安全,请运行从正规途径获取此软件,不要在不明来历的版本中输入你的豆瓣账户和密码,因为软件可能被黑客修改并植入窃取豆瓣账号的后门。你可以在 Github 上找到由官方发布的已编译的可执行版本,或者由源代码编译生成此软件。 10 | 11 | ## 开发技术 12 | 13 | 软件分为三个组件,分别是:用户界面、后台爬虫和后台数据库。其中用户界面基于 Electron 开发,后台爬虫使用 Python 语言编写。目前软件仅支持 Windows 平台,但是软件所使用的组件皆支持跨平台,不过由于作者偷懒并未做跨平台兼容性处理。 14 | 15 | 由于豆瓣对单个IP的访问频率有限制,所以软件的抓取速度并不会很快。可以通过设置多个代理服务器来加速抓取速度。 -------------------------------------------------------------------------------- /src/app/messenger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 与后台服务通信 3 | */ 4 | const EventEmitter = require('events') 5 | const WebSocket = require('ws') 6 | 7 | const SILENT_CLOSE_CODE = 892356 8 | 9 | /** 10 | * 支持重试连接的 WebSocket 客户端 11 | * 12 | * @extends EventEmitter 13 | */ 14 | class Messenger extends EventEmitter { 15 | 16 | /** 17 | * 创建 Messenger 18 | * 19 | * @param {string} address 20 | * @param {int} interval 21 | * @param {int} times 22 | */ 23 | constructor(address, interval=500, times=Number.MAX_SAFE_INTEGER) { 24 | super() 25 | this._address = address 26 | this.connect(interval, times) 27 | } 28 | 29 | /** 30 | * 连接服务器 31 | * 32 | * @param {int} interval 33 | * @param {int} times 34 | */ 35 | connect(interval, times) { 36 | let websocket = new WebSocket(this._address, { 37 | origin: 'localhost' 38 | }) 39 | websocket.on('error', (error) => { 40 | if (error.code == 'ECONNREFUSED') { 41 | if (times <= 0) { 42 | console.debug('[WebSocket]ECONNREFUSED: stop trying') 43 | this.emit('fail') 44 | return 45 | } 46 | 47 | console.debug(`[WebSocket]ECONNREFUSED: try to reconnect after ${interval}ms later. ${times - 1} times left`) 48 | 49 | setTimeout(() => { 50 | this.connect(interval, times - 1) 51 | }, interval) 52 | } else { 53 | console.debug('[WebSocket]error:' + error.message) 54 | } 55 | }) 56 | websocket.once('close', (code, reason) => { 57 | console.debug(`[WebSocket]closed: code: ${code} reason: ${reason}`) 58 | if (code == SILENT_CLOSE_CODE) { 59 | console.debug('slient closed') 60 | return 61 | } 62 | this.emit('close', code, reason) 63 | }) 64 | websocket.once('open', () => { 65 | console.debug('[WebSocket]opened') 66 | this.emit('open') 67 | }) 68 | websocket.on('message', (data) => { 69 | console.debug('[WebSocket]message received:' + data) 70 | this.emit('message', JSON.parse(data)) 71 | }) 72 | this._websocket = websocket 73 | } 74 | 75 | /** 76 | * 是否已经连接 77 | * 78 | * @returns {boolean} 79 | */ 80 | get isConnected() { 81 | let websocket = this._websocket 82 | return websocket.readyState === websocket.OPEN 83 | } 84 | 85 | /** 86 | * 关闭连接 87 | * 88 | * @param {boolean} silent 89 | */ 90 | close(silent=true) { 91 | this._websocket.close(SILENT_CLOSE_CODE) 92 | } 93 | 94 | /** 95 | * 终止连接 96 | */ 97 | terminate() { 98 | this._websocket.terminate() 99 | } 100 | 101 | /** 102 | * 发送消息 103 | * 104 | * @param {any} args 105 | */ 106 | send(...args) { 107 | this._websocket.send.apply(this._websocket, args) 108 | } 109 | } 110 | 111 | module.exports = Messenger -------------------------------------------------------------------------------- /src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doufen-app", 3 | "productName": "豆坟", 4 | "description": "A GUI shell for doufen platform", 5 | "version": "0.5.1", 6 | "main": "main.js", 7 | "author": "四不象(tabris17)", 8 | "commandName": "doufen", 9 | "homepage": "https://github.com/tabris17/doufen#readme", 10 | "urls": { 11 | "contact": "https://www.douban.com/people/tabris17/", 12 | "discuss": "https://github.com/tabris17/doufen/issues", 13 | "issues": "https://github.com/tabris17/doufen/issues" 14 | }, 15 | "dependencies": { 16 | "argparse": "^1.0.10", 17 | "cookie": "^0.3.1", 18 | "electron-referer": "^0.3.0", 19 | "jquery": "^3.3.1", 20 | "mustache": "^2.3.0", 21 | "showdown": "^1.8.6", 22 | "ws": "^5.1.1" 23 | }, 24 | "build": { 25 | "appId": "doufen.app", 26 | "productName": "doufen", 27 | "win": { 28 | "icon": "./app.ico", 29 | "target": ["portable", "zip"] 30 | }, 31 | "directories": { 32 | "output": "../../dist" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * renderer 进程调用的模块 3 | */ 4 | process.once('loaded', () => { 5 | const { shell, remote } = require('electron') 6 | const path = require('path') 7 | const EventEmitter = require('events') 8 | 9 | global.system = { 10 | require: global.require, 11 | module: global.module, 12 | 13 | /** 14 | * 在浏览器里打开链接 15 | * @param {string} url 16 | * @param {JSON} options 17 | * @param {Function} callback 18 | * @returns {boolean} 19 | */ 20 | openLink(url, options, callback) { 21 | return shell.openExternal(url, options, callback) 22 | }, 23 | 24 | /** 25 | * 返回程序路径 26 | * @returns {string} 27 | */ 28 | getAppPath() { 29 | return __dirname 30 | }, 31 | 32 | /** 33 | * 获得主进程共享数据 34 | */ 35 | getSharedData(name) { 36 | return remote.getGlobal('sharedData')[name] 37 | } 38 | } 39 | system.__proto__ = EventEmitter.prototype 40 | 41 | global.document.addEventListener('DOMContentLoaded', () => { 42 | const jquery = require('jquery') 43 | require('mustache').tags = ['${', '}'] 44 | global.$ = jquery 45 | system.emit('loaded') 46 | }) 47 | 48 | delete global.require 49 | delete global.module 50 | 51 | global.eval = () => { 52 | throw new Error('disabled') 53 | } 54 | }) -------------------------------------------------------------------------------- /src/app/window.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 管理程序的窗口 3 | */ 4 | const { app, BrowserWindow, Tray, Menu, dialog } = require('electron') 5 | const path = require('path') 6 | const url = require('url') 7 | 8 | const MAIN_WINDOW_HEIGHT = 640 9 | const MAIN_WINDOW_WIDTH = 1105 10 | const MAIN_WINDOW_MIN_HEIGHT = 580 11 | const MAIN_WINDOW_MIN_WIDTH = 800 12 | const MAIN_WINDOW_TITLE = '豆坟' 13 | const APP_ICON = path.join(__dirname, 'app.ico') 14 | const SPLASH_WINDOW_HEIGHT = 309 15 | const SPLASH_WINDOW_WIDTH = 500 16 | 17 | 18 | let mainWindow 19 | let systemTray 20 | 21 | 22 | /** 23 | * 创建闪屏窗口 24 | * 25 | * @returns {BrowserWindow} 26 | */ 27 | function splashScreen() { 28 | let win = new BrowserWindow({ 29 | width: SPLASH_WINDOW_WIDTH, 30 | height: SPLASH_WINDOW_HEIGHT, 31 | frame: false, 32 | show: true, 33 | resizable: false, 34 | movable: false, 35 | icon: APP_ICON 36 | }) 37 | win.loadURL(url.format({ 38 | pathname: path.join(__dirname, 'index.html'), 39 | protocol: 'file:', 40 | slashes: true 41 | })) 42 | 43 | return win 44 | } 45 | 46 | /** 47 | * 创建程序主窗口 48 | * 49 | * @returns {BrowserWindow} 50 | */ 51 | function createMainWindow() { 52 | let win = new BrowserWindow({ 53 | width: MAIN_WINDOW_WIDTH, 54 | height: MAIN_WINDOW_HEIGHT, 55 | minWidth: MAIN_WINDOW_MIN_WIDTH, 56 | minHeight: MAIN_WINDOW_MIN_HEIGHT, 57 | title: MAIN_WINDOW_TITLE, 58 | show: false, 59 | icon: APP_ICON, 60 | webPreferences: { 61 | preload: path.join(__dirname, 'render.js') 62 | } 63 | }) 64 | 65 | if (global.debugMode) { 66 | win.webContents.openDevTools() 67 | } 68 | 69 | win.on('closed', () => { 70 | if (systemTray) { 71 | systemTray.destroy() 72 | } 73 | mainWindow = systemTray = null 74 | }) 75 | 76 | win.on('close', (event) => { 77 | dialog.showMessageBox(win, { 78 | type: 'question', 79 | buttons: ['是(&Y)', '否(&N)'], 80 | defaultId: 1, 81 | cancelId: 1, 82 | noLink: true, 83 | title: '确认', 84 | normalizeAccessKeys: true, 85 | message: '是否退出程序?' 86 | }, (result) => { 87 | if (result != 0) return 88 | win.destroy() 89 | }) 90 | event.preventDefault() 91 | }) 92 | 93 | win.on('page-title-updated', (event, title) => { 94 | event.preventDefault() 95 | win.setTitle(title.trim() != MAIN_WINDOW_TITLE ? `${MAIN_WINDOW_TITLE} - ${title}` : MAIN_WINDOW_TITLE) 96 | }) 97 | 98 | win.on('minimize', (event) => { 99 | win.hide() 100 | }) 101 | 102 | win.webContents.on('crashed', () => { 103 | const dialogOptions = { 104 | type: 'info', 105 | buttons: ['重新加载(&R)', '退出程序(&X)'], 106 | defaultId: 1, 107 | cancelId: 1, 108 | noLink: true, 109 | title: '信息', 110 | normalizeAccessKeys: true, 111 | message: '程序遇到崩溃。' 112 | } 113 | dialog.showMessageBox(dialogOptions, (result) => { 114 | if (result == 0) mainWindow.reload() 115 | else app.quit() 116 | }) 117 | }) 118 | 119 | return mainWindow = win 120 | } 121 | 122 | 123 | /** 124 | * 创建系统托盘 125 | * 126 | * @returns {Tray} 127 | */ 128 | function createTray() { 129 | const toggleMainWindow = () => { 130 | mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show() 131 | } 132 | 133 | let tray = new Tray(APP_ICON) 134 | tray.setToolTip(MAIN_WINDOW_TITLE) 135 | tray.setContextMenu(Menu.buildFromTemplate([{ 136 | label: '显示/隐藏窗口(&H)', 137 | click: toggleMainWindow 138 | }, 139 | { type: 'separator' }, 140 | { 141 | label: '退出(&X)', 142 | click() { 143 | mainWindow.close() 144 | } 145 | } 146 | ])) 147 | tray.on('double-click', toggleMainWindow) 148 | 149 | return systemTray = tray 150 | } 151 | 152 | 153 | /** 154 | * 获取主窗口 155 | * 156 | * @returns {BrowserWindow | null} 157 | */ 158 | function getMainWindow() { 159 | if (!mainWindow || mainWindow.isDestroyed()) { 160 | mainWindow = null 161 | } 162 | return mainWindow 163 | } 164 | 165 | 166 | exports.splashScreen = splashScreen 167 | exports.createMainWindow = createMainWindow 168 | exports.getMainWindow = getMainWindow 169 | exports.createTray = createTray 170 | -------------------------------------------------------------------------------- /src/service/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from .handlers import * -------------------------------------------------------------------------------- /src/service/handlers/dashboard.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import json 3 | 4 | from db import Account 5 | from tasks import ALL_TASKS 6 | 7 | from .handlers import BaseRequestHandler 8 | 9 | 10 | class Index(BaseRequestHandler): 11 | """ 12 | 控制台 13 | """ 14 | def get(self): 15 | workers = self.server.workers 16 | pedding_tasks = self.server.tasks 17 | accounts = Account.select().where(Account.is_invalid == False) 18 | 19 | if not accounts.count(): 20 | self.redirect(self.reverse_url('settings.accounts.login')) 21 | return 22 | 23 | self.render('dashboard.html', workers=workers, 24 | pedding_tasks=pedding_tasks, accounts=accounts, all_tasks=ALL_TASKS.keys()) 25 | 26 | 27 | class RestartWorkers(BaseRequestHandler): 28 | """ 29 | 重启工作进程 30 | """ 31 | def post(self): 32 | self.server.stop_workers() 33 | self.server.start_workers() 34 | self.write('OK') 35 | 36 | 37 | class AddTask(BaseRequestHandler): 38 | """ 39 | 添加任务 40 | """ 41 | def post(self): 42 | tasks = json.loads(self.get_argument('tasks')) 43 | task_names = tasks['tasks'] 44 | account_ids = tasks['accounts'] 45 | 46 | if isinstance(task_names, list) and isinstance(account_ids, list): 47 | for task_name in task_names: 48 | task_type = ALL_TASKS.get(task_name) 49 | for account_id in account_ids: 50 | try: 51 | account = Account.get(Account.id == account_id) 52 | task = task_type(account) 53 | self.server.add_task(task) 54 | except Account.DoesNotExist: 55 | pass 56 | self.server.push_task() 57 | self.write('OK') 58 | -------------------------------------------------------------------------------- /src/service/handlers/handlers.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import json 3 | import logging 4 | 5 | import tornado 6 | from tornado.websocket import WebSocketHandler 7 | from tornado.web import RequestHandler 8 | from pyquery import PyQuery 9 | 10 | import db 11 | 12 | 13 | class NotFound(RequestHandler): 14 | """ 15 | 默认404页 16 | """ 17 | def get(self): 18 | self.render('errors/404.html') 19 | 20 | 21 | class BaseRequestHandler(RequestHandler): 22 | """ 23 | 默认继承 24 | """ 25 | 26 | @property 27 | def server(self): 28 | return self.application.server 29 | 30 | 31 | def get_current_user(self): 32 | """ 33 | 获取当前用户,没有则返回None 34 | """ 35 | if not hasattr(self, '_current_user'): 36 | try: 37 | self._current_user = db.Account.get_default().user 38 | except (db.Account.DoesNotExist, db.User.DoesNotExist): 39 | self._current_user = None 40 | 41 | return self._current_user 42 | 43 | def write_error(self, status_code, **kwargs): 44 | if status_code == 404: 45 | self.render('errors/404.html') 46 | else: 47 | self.write('Error') 48 | 49 | 50 | class Notifier(WebSocketHandler): 51 | """ 52 | 用于后台向前台图形程序发送通知 53 | """ 54 | 55 | def check_origin(self, origin): 56 | return origin == 'localhost' 57 | 58 | def on_message(self, message): 59 | logging.debug('receive message: "{0}"'.format(message)) 60 | 61 | def open(self): 62 | logging.debug('websocket open') 63 | self.application.register_client(self) 64 | 65 | def on_close(self): 66 | logging.debug('websocket close') 67 | self.application.unregister_client(self) 68 | 69 | 70 | class Main(BaseRequestHandler): 71 | """ 72 | 主页 73 | """ 74 | 75 | def get(self): 76 | self.render('index.html') 77 | 78 | 79 | class Shutdown(BaseRequestHandler): 80 | """ 81 | 退出系统 82 | """ 83 | def shutdown(self): 84 | self.server.stop_workers() 85 | tornado.ioloop.IOLoop.current().stop() 86 | 87 | def get(self): 88 | ioloop = tornado.ioloop.IOLoop.current() 89 | ioloop.add_callback(self.shutdown) 90 | self.write('OK') 91 | 92 | 93 | class Manual(BaseRequestHandler): 94 | """ 95 | 使用手册 96 | """ 97 | 98 | def get(self): 99 | self.render('manual.html') 100 | 101 | 102 | class Book(BaseRequestHandler): 103 | """ 104 | 书 105 | """ 106 | def get(self, douban_id): 107 | try: 108 | subject = db.Book.get(db.Book.douban_id == douban_id) 109 | history = db.BookHistorical.select().where(db.BookHistorical.id == subject.id) 110 | except db.Book.DoesNotExist: 111 | raise tornado.web.HTTPError(404) 112 | try: 113 | mine = db.MyBook.get(db.MyBook.book == subject, db.MyBook.user == self.get_current_user()) 114 | except db.MyBook.DoesNotExist: 115 | mine = None 116 | self.render('book.html', subject=subject, history=history, mine=mine) 117 | 118 | 119 | class Music(BaseRequestHandler): 120 | """ 121 | 音乐 122 | """ 123 | def get(self, douban_id): 124 | try: 125 | subject = db.Music.get(db.Music.douban_id == douban_id) 126 | history = db.MusicHistorical.select().where(db.MusicHistorical.id == subject.id) 127 | except db.Music.DoesNotExist: 128 | raise tornado.web.HTTPError(404) 129 | try: 130 | mine = db.MyMusic.get(db.MyMusic.music == subject, db.MyMusic.user == self.get_current_user()) 131 | except db.MyMusic.DoesNotExist: 132 | mine = None 133 | self.render('music.html', subject=subject, history=history, mine=mine) 134 | 135 | 136 | class Movie(BaseRequestHandler): 137 | """ 138 | 电影 139 | """ 140 | def get(self, douban_id): 141 | try: 142 | subject = db.Movie.get(db.Movie.douban_id == douban_id) 143 | history = db.MovieHistorical.select().where(db.MovieHistorical.id == subject.id) 144 | except db.Movie.DoesNotExist: 145 | raise tornado.web.HTTPError(404) 146 | try: 147 | mine = db.MyMovie.get(db.MyMovie.movie == subject, db.MyMovie.user == self.get_current_user()) 148 | except db.MyMovie.DoesNotExist: 149 | mine = None 150 | self.render('movie.html', subject=subject, history=history, mine=mine) 151 | 152 | 153 | class Broadcast(BaseRequestHandler): 154 | """ 155 | 广播 156 | """ 157 | def get(self, douban_id): 158 | try: 159 | subject = db.Broadcast.get(db.Broadcast.douban_id == douban_id) 160 | except db.Broadcast.DoesNotExist: 161 | raise tornado.web.HTTPError(404) 162 | 163 | comments = db.Comment.select().join(db.User).where( 164 | db.Comment.target_type == 'broadcast', 165 | db.Comment.target_douban_id == subject.douban_id 166 | ) 167 | 168 | self.render('broadcast.html', subject=subject, comments=comments) 169 | 170 | 171 | 172 | class User(BaseRequestHandler): 173 | """ 174 | 用户 175 | """ 176 | def get(self, douban_id): 177 | try: 178 | subject = db.User.get(db.User.douban_id == douban_id) 179 | history = db.UserHistorical.select().where(db.UserHistorical.id == subject.id) 180 | except db.User.DoesNotExist: 181 | raise tornado.web.HTTPError(404) 182 | 183 | is_follower = db.Follower.select().where( 184 | db.Follower.follower == subject, 185 | db.Follower.user == self.get_current_user() 186 | ).exists() 187 | 188 | is_following = db.Following.select().where( 189 | db.Following.following_user == subject, 190 | db.Following.user == self.get_current_user() 191 | ).exists() 192 | 193 | self.render('user.html', subject=subject, history=history, is_follower=is_follower, is_following=is_following) 194 | 195 | 196 | class Attachment(BaseRequestHandler): 197 | """ 198 | 从本地缓存载入附件 199 | """ 200 | 201 | def get(self, url): 202 | try: 203 | attachment = db.Attachment.get(db.Attachment.url == url) 204 | if attachment.local: 205 | self.redirect(self.reverse_url('cache', attachment.local)) 206 | return 207 | except db.Attachment.DoesNotExist: 208 | pass 209 | 210 | self.redirect(url) 211 | 212 | 213 | class Note(BaseRequestHandler): 214 | """ 215 | 日记 216 | """ 217 | def get(self, douban_id): 218 | try: 219 | subject = db.Note.get(db.Note.douban_id == douban_id) 220 | history = db.NoteHistorical.select().where(db.NoteHistorical.id == subject.id) 221 | except db.Note.DoesNotExist: 222 | raise tornado.web.HTTPError(404) 223 | 224 | comments = db.Comment.select().join(db.User).where( 225 | db.Comment.target_type == 'note', 226 | db.Comment.target_douban_id == subject.douban_id 227 | ) 228 | 229 | dom = PyQuery(subject.content) 230 | dom_iframe = dom('iframe') 231 | dom_iframe.before('

站外视频

'.format(dom_iframe.attr('src'))) 232 | dom_iframe.remove() 233 | dom('a').add_class('external-link') 234 | 235 | self.render('note.html', note=subject, comments=comments, content=dom) 236 | 237 | 238 | class PhotoPicture(BaseRequestHandler): 239 | """ 240 | 照片 241 | """ 242 | def get(self, douban_id): 243 | try: 244 | subject = db.PhotoPicture.get(db.PhotoPicture.douban_id == douban_id) 245 | history = db.PhotoPictureHistorical.select().where(db.PhotoPictureHistorical.id == subject.id) 246 | except db.PhotoPicture.DoesNotExist: 247 | raise tornado.web.HTTPError(404) 248 | 249 | comments = db.Comment.select().join(db.User).where( 250 | db.Comment.target_type == 'photo', 251 | db.Comment.target_douban_id == subject.douban_id 252 | ) 253 | 254 | self.render('photo.html', photo=subject, comments=comments) 255 | 256 | 257 | class PhotoAlbum(BaseRequestHandler): 258 | """ 259 | 相册 260 | """ 261 | def get(self, douban_id): 262 | try: 263 | subject = db.PhotoAlbum.get(db.PhotoAlbum.douban_id == douban_id) 264 | history = db.PhotoAlbumHistorical.select().where(db.PhotoAlbumHistorical.id == subject.id) 265 | except db.PhotoAlbum.DoesNotExist: 266 | raise tornado.web.HTTPError(404) 267 | 268 | photos = db.PhotoPicture.select().where(db.PhotoPicture.photo_album == subject) 269 | 270 | self.render('album.html', album=subject, photos=photos) 271 | 272 | -------------------------------------------------------------------------------- /src/service/handlers/my/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from .my import * -------------------------------------------------------------------------------- /src/service/handlers/search.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import logging 3 | 4 | from .handlers import BaseRequestHandler 5 | 6 | 7 | class Index(BaseRequestHandler): 8 | """ 9 | 搜索主页 10 | """ 11 | def get(self): 12 | 13 | self.render('search.html') -------------------------------------------------------------------------------- /src/service/handlers/settings/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from .settings import * -------------------------------------------------------------------------------- /src/service/handlers/settings/accounts.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import re 3 | from ..handlers import BaseRequestHandler 4 | from db import Account 5 | from tasks import SyncAccountTask 6 | 7 | class Login(BaseRequestHandler): 8 | """ 9 | 登录豆瓣 10 | """ 11 | 12 | def get(self): 13 | self.render('settings/accounts/login.html') 14 | 15 | 16 | class Index(BaseRequestHandler): 17 | """ 18 | 管理帐号 19 | """ 20 | 21 | def get(self): 22 | self.render('settings/accounts/index.html', rows=Account.select()) 23 | 24 | 25 | class Add(BaseRequestHandler): 26 | """ 27 | 添加帐号 28 | """ 29 | 30 | def post(self): 31 | session = self.get_argument('session') 32 | homepage = self.get_argument('homepage') 33 | 34 | try: 35 | name = re.findall(r'^http(?:s?)://www\.douban\.com/people/(.+)/$', homepage).pop(0) 36 | account = Account.get(Account.name == name) 37 | account.session = session 38 | account.is_invalid = False 39 | account.save() 40 | except Account.DoesNotExist: 41 | account = Account.create(session=session, name=name) 42 | except IndexError: 43 | self.write('FAIL') 44 | return 45 | 46 | self.server.add_task(SyncAccountTask(account)) 47 | self.server.push_task() 48 | 49 | self.write('OK') 50 | 51 | 52 | class Remove(BaseRequestHandler): 53 | """ 54 | 删除帐号 55 | """ 56 | def post(self): 57 | account_id = self.get_argument('id') 58 | try: 59 | Account.delete().where(Account.id == account_id).execute() 60 | except: 61 | pass 62 | self.write('OK') 63 | 64 | 65 | class Activate(BaseRequestHandler): 66 | """ 67 | 激活帐号 68 | """ 69 | def post(self): 70 | account_id = self.get_argument('id') 71 | try: 72 | Account.update(is_activated=False).execute() 73 | Account.update(is_activated=True).where(Account.id == account_id).execute() 74 | except: 75 | pass 76 | self.write('OK') -------------------------------------------------------------------------------- /src/service/handlers/settings/settings.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from ..handlers import BaseRequestHandler 3 | import setting 4 | from worker import REQUESTS_PER_MINUTE, LOCAL_OBJECT_DURATION, BROADCAST_ACTIVE_DURATION, BROADCAST_INCREMENTAL_BACKUP, IMAGE_LOCAL_CACHE 5 | 6 | 7 | class General(BaseRequestHandler): 8 | """ 9 | 设置 10 | """ 11 | 12 | def get(self, flash=''): 13 | requests_per_minute = setting.get('worker.requests-per-minute', int, REQUESTS_PER_MINUTE) 14 | local_object_duration = setting.get('worker.local-object-duration', int, LOCAL_OBJECT_DURATION) 15 | broadcast_active_duration = setting.get('worker.broadcast-active-duration', int, BROADCAST_ACTIVE_DURATION) 16 | broadcast_incremental_backup = setting.get('worker.broadcast-incremental-backup', bool, BROADCAST_INCREMENTAL_BACKUP) 17 | image_local_cache = setting.get('worker.image-local-cache', bool, IMAGE_LOCAL_CACHE) 18 | self.render( 19 | 'settings/general.html', 20 | requests_per_minute=requests_per_minute, 21 | local_object_duration=int(local_object_duration / (60 * 60 *24)), 22 | broadcast_active_duration=int(broadcast_active_duration / (60 * 60 *24)), 23 | broadcast_incremental_backup=broadcast_incremental_backup, 24 | image_local_cache=image_local_cache, 25 | flash=flash 26 | ) 27 | 28 | def post(self): 29 | requests_per_minute = self.get_argument('requests-per-minute') 30 | setting.set('worker.requests-per-minute', requests_per_minute, int) 31 | 32 | local_object_duration_days = int(self.get_argument('local-object-duration')) 33 | local_object_duration = local_object_duration_days * 60 * 60 *24 34 | setting.set('worker.local-object-duration', local_object_duration, int) 35 | 36 | broadcast_active_duration_days = int(self.get_argument('broadcast-active-duration')) 37 | broadcast_active_duration = broadcast_active_duration_days * 60 * 60 *24 38 | setting.set('worker.broadcast-active-duration', broadcast_active_duration, int) 39 | 40 | broadcast_incremental_backup = int(self.get_argument('broadcast-incremental-backup')) 41 | setting.set('worker.broadcast-incremental-backup', broadcast_incremental_backup, bool) 42 | 43 | image_local_cache = int(self.get_argument('image-local-cache')) 44 | setting.set('worker.image-local-cache', image_local_cache, bool) 45 | 46 | return self.get('需要重启工作进程或者程序才能使设置生效') 47 | 48 | 49 | class Network(BaseRequestHandler): 50 | """ 51 | 设置 52 | """ 53 | 54 | def get(self, flash=''): 55 | proxies = setting.get('worker.proxies', 'json', []) 56 | self.render('settings/network.html', proxies='\n'.join(proxies), flash=flash) 57 | 58 | def post(self): 59 | proxies = self.get_argument('proxies').split('\n') 60 | proxies = [proxy.strip() for proxy in list(set(proxies)) if proxy.strip()] 61 | setting.set('worker.proxies', proxies, 'json') 62 | return self.get('需要重启工作进程或者程序才能使设置生效') 63 | -------------------------------------------------------------------------------- /src/service/main.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import argparse 3 | import logging 4 | import os 5 | import sys 6 | import time 7 | 8 | import db 9 | import version 10 | from server import Server 11 | from worker import Worker 12 | from setting import settings, DEFAULT_SERVICE_PORT, DEFAULT_DATEBASE, DEFAULT_CACHE_PATH, DEFAULT_SERVICE_HOST, DEFAULT_LOG_PATH, DEFAULT_DEBUG_MODE, DEFAULT_SILENT_MODE 13 | 14 | 15 | def parse_args(args): 16 | """ 17 | 解析命令参数 18 | """ 19 | parser = argparse.ArgumentParser( 20 | prog=version.__prog_name__, 21 | description=version.__description__ 22 | ) 23 | parser.add_argument('-v', '--version', action='version', 24 | version='%(prog)s ' + version.__version__) 25 | parser.add_argument('-d', '--debug', action='store_true', 26 | default=DEFAULT_DEBUG_MODE, help='print debug information') 27 | parser.add_argument('-p', '--port', type=int, default=DEFAULT_SERVICE_PORT, 28 | metavar='port', help='specify the port to listen') 29 | parser.add_argument('-s', '--save', default=DEFAULT_DATEBASE, 30 | metavar='database', dest='database', help='specify the database file') 31 | parser.add_argument('-c', '--cache', default=DEFAULT_CACHE_PATH, 32 | metavar='cache', dest='cache', help='specify the cache path') 33 | parser.add_argument('-l', '--log', default=DEFAULT_LOG_PATH, 34 | metavar='log', dest='log', help='specify the log files path') 35 | parser.add_argument('-q', '--quiet', action='store_true', 36 | default=DEFAULT_SILENT_MODE, help='switch on silent mode') 37 | 38 | return parser.parse_args(args) 39 | 40 | 41 | def init_env(): 42 | db_path = os.path.dirname(settings.get('database')) 43 | if not os.path.exists(db_path): 44 | os.makedirs(db_path) 45 | 46 | cache_path = settings.get('cache') 47 | if not os.path.exists(cache_path): 48 | os.makedirs(cache_path) 49 | 50 | log_path = settings.get('log') 51 | if not os.path.exists(log_path): 52 | os.makedirs(log_path) 53 | 54 | 55 | def init_logger(): 56 | logging_handlers = [ 57 | logging.handlers.TimedRotatingFileHandler( 58 | os.path.join(settings.get('log'), 'service.log'), 59 | when='D' 60 | ) 61 | ] 62 | if not settings.get('quiet'): 63 | logging_handlers.append(logging.StreamHandler()) 64 | 65 | logging.basicConfig( 66 | level=logging.DEBUG if settings.get('debug') else logging.INFO, 67 | format='[%(asctime)s] (%(pathname)s:%(lineno)s) [%(levelname)s] %(name)s: %(message)s', 68 | datefmt='%m-%d %H:%M', 69 | handlers=logging_handlers 70 | ) 71 | 72 | 73 | def main(args): 74 | """ 75 | 程序主函数 76 | """ 77 | parsed_args = parse_args(args) 78 | 79 | settings.update({ 80 | 'cache': parsed_args.cache, 81 | 'log': parsed_args.log, 82 | 'database': parsed_args.database, 83 | 'port': parsed_args.port, 84 | 'debug': parsed_args.debug, 85 | 'quiet': parsed_args.quiet, 86 | }) 87 | 88 | init_env() 89 | init_logger() 90 | 91 | db.init(parsed_args.database) 92 | 93 | server = Server(parsed_args.port, DEFAULT_SERVICE_HOST, parsed_args.cache) 94 | server.run() 95 | 96 | 97 | if __name__ == '__main__': 98 | try: 99 | import multiprocessing 100 | multiprocessing.freeze_support() # for PyInstaller 101 | main(sys.argv[1:]) 102 | except (KeyboardInterrupt, SystemExit): 103 | print('exit') 104 | -------------------------------------------------------------------------------- /src/service/service.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabris17/doufen/5de4212f50cc2b31423db0a0ed86ca82bed05213/src/service/service.ico -------------------------------------------------------------------------------- /src/service/setting.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import json 3 | from db import dbo, Setting 4 | 5 | 6 | DEFAULT_SERVICE_PORT = 8398 7 | DEFAULT_SERVICE_HOST = '127.0.0.1' 8 | DEFAULT_DATEBASE = 'var/data/graveyard.db' 9 | DEFAULT_CACHE_PATH = 'var/cache' 10 | DEFAULT_LOG_PATH = 'var/log' 11 | DEFAULT_DEBUG_MODE = False 12 | DEFAULT_SILENT_MODE = False 13 | 14 | settings = { 15 | 'debug': DEFAULT_DEBUG_MODE, 16 | 'quiet': DEFAULT_SILENT_MODE, 17 | 'cache': DEFAULT_CACHE_PATH, 18 | 'log': DEFAULT_LOG_PATH, 19 | 'database': DEFAULT_DATEBASE, 20 | 'port': DEFAULT_SERVICE_PORT, 21 | } 22 | 23 | def get(name, value_type=str, default=None): 24 | try: 25 | setting = Setting.get(Setting.name == name) 26 | value = setting.value 27 | if value_type == 'json': 28 | return json.loads(value) 29 | elif value_type is bool: 30 | return bool(int(value)) 31 | else: 32 | return value_type(value) 33 | except (Setting.DoesNotExist, ValueError): 34 | return default 35 | 36 | 37 | def set(name, value, value_type=str): 38 | try: 39 | if value_type == 'json': 40 | value_formated = json.dumps(value) 41 | elif value_type is bool: 42 | value_formated = 1 if value else 0 43 | else: 44 | value_formated = value_type(value) 45 | except ValueError: 46 | return False 47 | 48 | Setting.insert(name=name, value=value_formated).on_conflict_replace().execute() 49 | return True 50 | -------------------------------------------------------------------------------- /src/service/static/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabris17/doufen/5de4212f50cc2b31423db0a0ed86ca82bed05213/src/service/static/404.jpg -------------------------------------------------------------------------------- /src/service/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabris17/doufen/5de4212f50cc2b31423db0a0ed86ca82bed05213/src/service/static/logo.png -------------------------------------------------------------------------------- /src/service/static/photo_album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabris17/doufen/5de4212f50cc2b31423db0a0ed86ca82bed05213/src/service/static/photo_album.png -------------------------------------------------------------------------------- /src/service/static/star_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabris17/doufen/5de4212f50cc2b31423db0a0ed86ca82bed05213/src/service/static/star_matrix.png -------------------------------------------------------------------------------- /src/service/static/theme.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | font: 14px Helvetica, Arial, "Microsoft YaHei", sans-serif; 3 | } 4 | 5 | #primary-nav { 6 | font-size: 1.1rem; 7 | } 8 | 9 | #primary-nav>.navbar-brand>.navbar-item { 10 | font-size: 2rem; 11 | line-height: 2rem; 12 | } 13 | 14 | #primary-nav>.navbar-brand>.navbar-item .icon { 15 | margin-right: 1rem; 16 | } 17 | 18 | .section { 19 | padding: 2rem 1.5rem; 20 | } 21 | 22 | .has-margin { 23 | margin: 1rem; 24 | } 25 | 26 | .has-padding { 27 | padding: 1rem; 28 | } 29 | 30 | dl.is-horizontal>dt, dl.is-horizontal>dd { 31 | display: block; 32 | line-height: 1.5; 33 | } 34 | 35 | dl.is-horizontal>dt { 36 | font-weight: 600; 37 | float: left; 38 | width: 120px; 39 | overflow: hidden; 40 | clear: left; 41 | text-align: right; 42 | text-overflow: ellipsis; 43 | white-space: nowrap; 44 | } 45 | 46 | dl.is-horizontal>dd { 47 | margin-left: 140px; 48 | } 49 | 50 | dl.is-horizontal.is-label-size-4>dt{ 51 | width: 60px; 52 | text-align: left; 53 | } 54 | 55 | 56 | dl.is-horizontal.is-label-size-4>dt{ 57 | width: 4rem; 58 | } 59 | dl.is-horizontal.is-label-size-4>dd { 60 | margin-left: 4.5rem; 61 | } 62 | 63 | .text-break{ 64 | overflow-wrap: break-word; 65 | word-break: break-all; 66 | white-space: normal; 67 | word-wrap: break-word; 68 | overflow-x: hidden; 69 | text-overflow: ellipsis; 70 | } 71 | 72 | .rating-start { 73 | display: inline-block; 74 | width: 55px; 75 | height: 11px; 76 | margin-right: 4px; 77 | background-image: url('star_matrix.png'); 78 | } 79 | 80 | 81 | .rating-start.star-50 { 82 | background-position: 0 0px 83 | } 84 | 85 | .rating-start.star-45 { 86 | background-position: 0 -11px 87 | } 88 | 89 | .rating-start.star-40 { 90 | background-position: 0 -22px 91 | } 92 | 93 | .rating-start.star-35 { 94 | background-position: 0 -33px 95 | } 96 | 97 | .rating-start.star-30 { 98 | background-position: 0 -44px 99 | } 100 | 101 | .rating-start.star-25 { 102 | background-position: 0 -55px 103 | } 104 | 105 | .rating-start.star-20 { 106 | background-position: 0 -66px 107 | } 108 | 109 | .rating-start.star-15 { 110 | background-position: 0 -77px 111 | } 112 | 113 | .rating-start.star-10 { 114 | background-position: 0 -88px 115 | } 116 | 117 | .rating-start.star-05 { 118 | background-position: 0 -99px 119 | } 120 | 121 | .rating-start.star-00 { 122 | background-position: 0 -110px 123 | } 124 | 125 | ul.menu>li { 126 | margin: 0 0 10px 0; 127 | } 128 | -------------------------------------------------------------------------------- /src/service/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from .tasks import * -------------------------------------------------------------------------------- /src/service/tasks/exceptions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | 4 | class Forbidden(Exception): 5 | """ 6 | 登录会话或IP被屏蔽了 7 | """ 8 | pass -------------------------------------------------------------------------------- /src/service/tests.py: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | import logging 3 | 4 | import db 5 | import tasks 6 | 7 | 8 | logging.basicConfig( 9 | level=logging.INFO, 10 | format='[%(asctime)s] (%(pathname)s:%(lineno)s) [%(levelname)s] %(name)s: %(message)s', 11 | datefmt='%m-%d %H:%M' 12 | ) 13 | db.init('var/data/graveyard.db') 14 | 15 | 16 | class DownloadPictureTask(tasks.LikeTask): 17 | """ 18 | 下载图片任务 19 | """ 20 | 21 | def run(self): 22 | if self._image_local_cache: 23 | while self.fetch_attachment(): 24 | pass 25 | 26 | 27 | class TestTask(tasks.BroadcastTask): 28 | """ 29 | 测试任务 30 | """ 31 | def run(self): 32 | self.fetch_photo_album(1671066229) 33 | 34 | 35 | #task = tasks.FollowingFollowerTask(db.Account.get_by_id(1)) 36 | #task = tasks.BroadcastCommentTask(db.Account.get_by_id(1)) 37 | #task = tasks.PhotoAlbumTask(db.Account.get_by_id(1)) 38 | task = tasks.NoteTask(db.Account.get_by_id(1)) 39 | #task = tasks.LikeTask(db.Account.get_by_id(1)) 40 | #task = tasks.ReviewTask(db.Account.get_by_id(1)) 41 | #task = DownloadPictureTask(db.Account.get_by_id(1)) 42 | #task = TestTask(db.Account.get_by_id(1)) 43 | #task = tasks.BroadcastTask(db.Account.get_by_id(1)) 44 | 45 | result = task( 46 | requests_per_minute=30, 47 | local_object_duration=60*60*24*300, 48 | broadcast_active_duration=60*60*24*10, 49 | broadcast_incremental_backup=True, 50 | image_local_cache=True 51 | ) 52 | print(result) 53 | -------------------------------------------------------------------------------- /src/service/uimodules.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import tornado 3 | import db 4 | 5 | class Account(tornado.web.UIModule): 6 | """ 7 | 登录帐号模块 8 | """ 9 | 10 | def render(self): 11 | try: 12 | account = db.Account.get_default() 13 | except (db.Account.DoesNotExist, db.User.DoesNotExist): 14 | account = None 15 | return self.render_string('modules/account.html', account=account) 16 | 17 | 18 | class Movie(tornado.web.UIModule): 19 | """ 20 | 电影卡片 21 | """ 22 | 23 | def render(self, douban_id): 24 | try: 25 | movie = db.Movie.get(db.Movie.douban_id == douban_id) 26 | except db.Movie.DoesNotExist: 27 | movie = None 28 | return self.render_string('modules/movie.html', movie=movie) 29 | 30 | 31 | 32 | class Book(tornado.web.UIModule): 33 | """ 34 | 图书卡片 35 | """ 36 | 37 | def render(self, douban_id): 38 | try: 39 | book = db.Book.get(db.Book.douban_id == douban_id) 40 | except db.Book.DoesNotExist: 41 | book = None 42 | return self.render_string('modules/book.html', book=book) 43 | 44 | 45 | class Music(tornado.web.UIModule): 46 | """ 47 | 音乐卡片 48 | """ 49 | 50 | def render(self, douban_id): 51 | try: 52 | music = db.Music.get(db.Music.douban_id == douban_id) 53 | except db.Music.DoesNotExist: 54 | music = None 55 | return self.render_string('modules/music.html', music=music) 56 | 57 | 58 | class Note(tornado.web.UIModule): 59 | """ 60 | 日记卡片 61 | """ 62 | 63 | def render(self, douban_id): 64 | try: 65 | note = db.Note.get(db.Note.douban_id == douban_id) 66 | except db.Note.DoesNotExist: 67 | note = None 68 | return self.render_string('modules/note.html', note=note) 69 | 70 | 71 | class User(tornado.web.UIModule): 72 | """ 73 | 用户卡片 74 | """ 75 | 76 | def render(self, douban_id): 77 | try: 78 | user = db.User.get(db.User.douban_id == douban_id) 79 | except db.User.DoesNotExist: 80 | user = None 81 | return self.render_string('modules/user.html', user=user) 82 | 83 | 84 | class Recommend(tornado.web.UIModule): 85 | """ 86 | 用户卡片 87 | """ 88 | 89 | def render(self, recommend, broadcast): 90 | return self.render_string('modules/recommend.html', recommend=recommend, broadcast=broadcast) 91 | -------------------------------------------------------------------------------- /src/service/urls.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import handlers 3 | import handlers.settings 4 | import handlers.settings.accounts 5 | import handlers.dashboard 6 | import handlers.my 7 | import handlers.exports 8 | import handlers.search 9 | 10 | 11 | patterns = [ 12 | (r'/', handlers.Main, None, 'index'), 13 | (r'/shutdown', handlers.Shutdown, None, 'shutdown'), 14 | (r'/attachment/(.+)', handlers.Attachment, None, 'attachment'), 15 | (r'/settings', handlers.settings.General, None, 'settings'), 16 | (r'/settings/general', handlers.settings.General, None, 'settings.general'), 17 | (r'/settings/network', handlers.settings.Network, None, 'settings.network'), 18 | (r'/settings/accounts/', handlers.settings.accounts.Index, None, 'settings.accounts'), 19 | (r'/settings/accounts/login', handlers.settings.accounts.Login, None, 'settings.accounts.login'), 20 | (r'/settings/accounts/add', handlers.settings.accounts.Add, None, 'settings.accounts.add'), 21 | (r'/settings/accounts/remove', handlers.settings.accounts.Remove, None, 'settings.accounts.remove'), 22 | (r'/settings/accounts/activate', handlers.settings.accounts.Activate, None, 'settings.accounts.activate'), 23 | (r'/dashboard', handlers.dashboard.Index, None, 'dashboard'), 24 | (r'/dashboard/workers/restart', handlers.dashboard.RestartWorkers, None, 'dashboard.workers.restart'), 25 | (r'/dashboard/tasks/add', handlers.dashboard.AddTask, None, 'dashboard.tasks.add'), 26 | (r'/help/manual', handlers.Manual, None, 'help.manual'), 27 | (r'/notify', handlers.Notifier, None, 'notify'), 28 | (r'/my', handlers.my.Index, None, 'my'), 29 | (r'/my/following', handlers.my.Following, None, 'my.following'), 30 | (r'/my/following/historical', handlers.my.FollowingHistorical, None, 'my.following.historical'), 31 | (r'/my/followers', handlers.my.Followers, None, 'my.followers'), 32 | (r'/my/followers/historical', handlers.my.FollowersHistorical, None, 'my.followers.historical'), 33 | (r'/my/blocklist', handlers.my.Blocklist, None, 'my.blocklist'), 34 | (r'/my/blocklist/historical', handlers.my.BlocklistHistorical, None, 'my.blocklist.historical'), 35 | (r'/my/book/([^/]+)', handlers.my.Book, None, 'my.book'), 36 | (r'/my/book/historical', handlers.my.BookHistorical, None, 'my.book.historical'), 37 | (r'/my/music/([^/]+)', handlers.my.Music, None, 'my.music'), 38 | (r'/my/music/historical', handlers.my.MusicHistorical, None, 'my.music.historical'), 39 | (r'/my/movie/([^/]+)', handlers.my.Movie, None, 'my.movie'), 40 | (r'/my/movie/historical', handlers.my.MovieHistorical, None, 'my.movie.historical'), 41 | (r'/my/broadcast', handlers.my.Broadcast, None, 'my.broadcast'), 42 | (r'/my/note', handlers.my.Note, None, 'my.note'), 43 | (r'/my/photo', handlers.my.Photo, None, 'my.photo'), 44 | (r'/my/favorite/([^/]*)', handlers.my.Favorite, None, 'my.favorite'), 45 | (r'/note/([^/]+)', handlers.Note, None, 'note'), 46 | (r'/book/([^/]+)', handlers.Book, None, 'book'), 47 | (r'/movie/([^/]+)', handlers.Movie, None, 'movie'), 48 | (r'/music/([^/]+)', handlers.Music, None, 'music'), 49 | (r'/broadcast/([^/]+)', handlers.Broadcast, None, 'broadcast'), 50 | (r'/user/([^/]+)', handlers.User, None, 'user'), 51 | (r'/photo/([^/]+)', handlers.PhotoPicture, None, 'photo'), 52 | (r'/photo/album/([^/]+)', handlers.PhotoAlbum, None, 'photo.album'), 53 | (r'/exports/', handlers.exports.Index, None, 'exports'), 54 | (r'/search', handlers.search.Index, None, 'search'), 55 | ] 56 | -------------------------------------------------------------------------------- /src/service/version.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | 4 | __version__ = '0.2.0' 5 | __prog_name__ = 'gravekeeper' 6 | __description__ = 'Backend process for doufen platform' 7 | __author__ = '四不象(tabris17)' 8 | -------------------------------------------------------------------------------- /src/service/views/album.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{ album.title }}{% end %} 4 | 5 | {% block main %} 6 | {% from pyquery import PyQuery %} 7 |
8 | 25 | 26 |
27 |

28 | {{ album.title }} 29 |

30 | {% if album.last_updated %} 31 |

32 | {{ album.last_updated }} 33 |

34 | {% end %} 35 |

36 | {{ album.photos_count }}张照片 37 | {% if album.views_count %} 38 | {{ album.views_count }}人浏览 39 | {% end %} 40 | {% if album.like_count %} 41 | 喜欢({{ album.like_count }}) 42 | {% end %} 43 | {% if album.rec_count %} 44 | 推荐({{ album.rec_count }}) 45 | {% end %} 46 |

47 |

{{ album.desc }}

48 |
49 | {% set i = 1 %} 50 |
51 | {% for row in photos %} 52 |
53 |
54 | 55 |
56 |

{{ row.desc }}

57 |
58 | {% if i % 6 == 0 %} 59 |
60 |
61 | {% end %} 62 | {% set i += 1 %} 63 | {% end %} 64 |
65 |
66 | {% end %} 67 | -------------------------------------------------------------------------------- /src/service/views/book.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{ subject.title }}{% end %} 4 | 5 | {% block main %} 6 | {% import ast %} 7 |
8 | 25 |
26 |
27 |

28 | 29 |

30 |
31 |
32 |
33 |

34 | {{ subject.title }} 35 | {{ subject.subtitle }} 36 | {% if subject.alt_title %} 37 |
({{ subject.alt_title }}) 38 | {% end %} 39 |

40 |
41 | {% if subject.author %} 42 | {% set author = ast.literal_eval(subject.author) %} 43 | {% if len(subject.author) %} 44 |
作者
45 |
{{ ' / '.join(author) }}
46 | {% end %} 47 | {% end %} 48 | 49 | {% if subject.translator %} 50 | {% set translator = ast.literal_eval(subject.translator) %} 51 | {% if len(translator) %} 52 |
译者
53 |
{{ ' / '.join(translator) }}
54 | {% end %} 55 | {% end %} 56 | 57 | {% if subject.publisher %} 58 |
出版社
59 |
{{ subject.publisher }}
60 | {% end %} 61 | 62 | {% if subject.origin_title %} 63 |
原作名
64 |
{{ subject.origin_title }}
65 | {% end %} 66 | 67 | {% if subject.pubdate %} 68 |
出版日期
69 |
{{ subject.pubdate }}
70 | {% end %} 71 | 72 | {% if subject.isbn10 or subject.isbn13 %} 73 |
ISBN
74 |
{{ subject.isbn10 }} / {{ subject.isbn13 }}
75 | {% end %} 76 | 77 | {% if subject.price %} 78 |
价格
79 |
{{ subject.price }}
80 | {% end %} 81 | 82 | {% if subject.pages %} 83 |
页数
84 |
{{ subject.pages }}
85 | {% end %} 86 | 87 | {% if subject.binding %} 88 |
装帧
89 |
{{ subject.binding }}
90 | {% end %} 91 |
92 | {% if mine %} 93 |
94 |
我的评价
95 |
96 | {% if mine.rating %} 97 | {% set my_rating = ast.literal_eval(mine.rating) %} 98 | 99 | {% else %} 100 | 101 | {% end %} 102 | {{ mine.create_time }} 103 |
104 | {% if mine.tags %} 105 | {% set tags = ast.literal_eval(mine.tags) %} 106 | {% if len(tags) %} 107 |
标签
108 |
{{ ' / '.join(tags) }}
109 | {% end %} 110 | {% end %} 111 |
112 |

{{ mine.comment }}

113 | {% end %} 114 |
115 |
116 |
117 |

118 | {% if subject.rating %} 119 | {% set rating = ast.literal_eval(subject.rating) %} 120 | 豆瓣评分 {{ rating['average'] }} / 10
121 | {{ rating['numRaters'] }} 人评价 122 | {% end %} 123 |

124 |
125 |
126 | 127 |
128 | {% if subject.summary %} 129 |

内容简介

130 | {% for ln in subject.summary.split("\n") %} 131 | {% if ln %} 132 |

{{ ln }}

133 | {% end %} 134 | {% end %} 135 | {% end %} 136 | 137 | {% if subject.author_intro %} 138 |

作者简介

139 | {% for ln in subject.author_intro.split("\n") %} 140 | {% if ln %} 141 |

{{ ln }}

142 | {% end %} 143 | {% end %} 144 | {% end %} 145 | 146 | {% if subject.catalog %} 147 |

目录

148 |

149 | {% for ln in subject.catalog.split("\n") %} 150 | {% if ln %} 151 | {{ ln }}
152 | {% end %} 153 | {% end %} 154 |

155 | {% end %} 156 | 157 | {% if subject.tags %} 158 | {% set tags = ast.literal_eval(subject.tags) %} 159 |

豆瓣成员常用的标签

160 | {% for tag in tags %} 161 | {{ tag['name'] }}({{ tag['count'] }}) 162 | {% end %} 163 | {% end %} 164 |
165 | 166 |
167 | {% end %} 168 | -------------------------------------------------------------------------------- /src/service/views/broadcast.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{subject.user.name}}的广播{% end %} 4 | 5 | {% block main %} 6 | {% from pyquery import PyQuery %} 7 | {% import ast %} 8 |
9 | 26 | 27 | {% module Template('my/_broadcast.html', boxed=False, wrapped=False, broadcast=subject) %} 28 | 29 |
30 | 31 | {% for comment in comments %} 32 |
33 |
34 |

35 | 36 |

37 |
38 |
39 |

40 | {{ comment.user.name }} 41 | (@{{ comment.user.unique_name}}) 42 | {{ comment.user.signature }} 43 | {{ comment.created}} 44 |

45 |

{{ comment.text }}

46 |
47 |
48 | {% end %} 49 |
50 | {% end %} 51 | -------------------------------------------------------------------------------- /src/service/views/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}控制台{% end %} 4 | 5 | {% block main %} 6 |
7 | 17 | 18 |
19 |
20 |
21 |

工作进程

22 | {% if len(workers) == 0 %} 23 |

进程未启动

24 | {% else %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% for worker in workers %} 35 | 36 | 37 | 38 | 39 | 40 | {% end %} 41 | 42 |
名称状态任务
{{ worker.name }}{{ worker.status_text }}{{ worker.current_task }}
43 |

44 | 重启 45 | 刷新 46 |

47 | {% end %} 48 |
49 |
50 |

任务

51 | {% if len(pedding_tasks) == 0 %} 52 |

没有等待运行的任务

53 | {% else %} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {% for task in pedding_tasks %} 63 | 64 | 65 | 66 | {% end %} 67 | 68 |
名称
{{ task.name }}
69 | {% end %} 70 |

71 | 新建 72 | 刷新 73 |

74 |
75 |
76 |
77 |
78 |

日志

79 | 80 |
81 |
82 |
83 | 84 | 125 | 126 |
127 | {% end %} 128 | 129 | 130 | {% block body_extra %} 131 | 199 | {% end %} 200 | -------------------------------------------------------------------------------- /src/service/views/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "../themes/base.html" %} 2 | 3 | 4 | {% block body %} 5 |
6 |
7 |
8 |

返回

9 |

404

10 |

Page Not Found

11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | {% end %} -------------------------------------------------------------------------------- /src/service/views/exports.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}导出 Excel{% end %} 4 | 5 | {% block body_extra %} 6 | 42 | {% end %} 43 | 44 | {% block main %} 45 |
46 | 58 | 59 | 60 | 81 | 82 |
83 | {% end %} 84 | -------------------------------------------------------------------------------- /src/service/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/base.html" %} 2 | 3 | 4 | {% block body %} 5 |
6 |
7 |
8 |

豆瓣备份机

9 |

如果豆瓣是你的精神病角落,那你需要备份一下你的病历吗?

10 |
11 |
12 |
13 | 14 |
15 |
16 |

第一步

17 |

登录你的豆瓣帐号。

18 |

注意:在软件备份过程中,你的帐号可能会被豆瓣误判为访问异常而被锁定。帐号被锁定后需要到豆瓣网站进行解锁。解锁过程需要用到手机发送验证码。如果你无法使用注册手机号发送验证码将会导致账户无法解锁。故请谨慎使用!

19 |

第二步

20 |

控制台新建备份任务。

21 |

在“控制台”页面的“任务”一栏里,点击“新建”按钮。在对话框左侧选中你的帐号,右侧选择要执行的备份任务。

22 |

第三步

23 |

我的豆瓣查看备份结果。

24 |
25 |
26 | 27 | {% end %} -------------------------------------------------------------------------------- /src/service/views/manual.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}帮助{% end %} 4 | 5 | {% block head_extra %} 6 | 9 | {% end %} 10 | 11 | {% block main %} 12 |
13 | 23 | 24 |
25 |
26 | {% end %} 27 | 28 | {% block body_extra %} 29 | 41 | {% end %} 42 | -------------------------------------------------------------------------------- /src/service/views/modules/account.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/service/views/modules/book.html: -------------------------------------------------------------------------------- 1 | 2 | {% import ast %} 3 | 4 | {% if book %} 5 |
6 |
7 |

8 | 9 |

10 |
11 |
12 |
13 |

14 | {{ book.title }} 15 | {% if book.alt_title %}({{ book.alt_title }}){% end %} 16 |

17 |

{{ book.summary[0:100] }}...

18 |
19 | {% if book.author %} 20 | {% set author = ast.literal_eval(book.author) %} 21 | {% if len(author) %} 22 |
作者
23 |
{{ ' / '.join(author) }}
24 | {% end %} 25 | {% end %} 26 | 27 | {% if book.publisher %} 28 |
出版社
29 |
{{ book.publisher }}
30 | {% end %} 31 |
32 |
33 |
34 |
35 |

36 | {% if book.rating %} 37 | {% set rating = ast.literal_eval(book.rating) %} 38 | 豆瓣评分 {{ rating['average'] }} / 10
39 | {{ rating['numRaters'] }} 人评价 40 | {% end %} 41 |

42 |

43 | 查看详情 44 |

45 |
46 |
47 | {% else %} 48 |

图书可能已被删除

49 | {% end %} -------------------------------------------------------------------------------- /src/service/views/modules/movie.html: -------------------------------------------------------------------------------- 1 | 2 | {% import ast %} 3 | 4 | {% if movie %} 5 |
6 |
7 |

8 | 9 |

10 |
11 |
12 |
13 |

14 | {{ movie.title }} 15 | {% if movie.alt_title %}({{ movie.alt_title }}){% end %} 16 |

17 | {% set attrs = ast.literal_eval(movie.attrs) %} 18 |

{{ movie.summary[0:100] }}...

19 |
20 | {% if 'director' in attrs and attrs['director'] %} 21 |
导演
22 |
{{ ' / '.join(attrs['director']) }}
23 | {% end %} 24 | 25 | {% if 'cast' in attrs and attrs['cast'] %} 26 |
主演
27 |
{{ ' / '.join(attrs['cast']) }}
28 | {% end %} 29 | 30 | {% if 'movie_type' in attrs and attrs['movie_type'] %} 31 |
类型
32 |
{{ ' / '.join(attrs['movie_type']) }}
33 | {% end %} 34 |
35 |
36 |
37 |
38 |

39 | {% if movie.rating %} 40 | {% set rating = ast.literal_eval(movie.rating) %} 41 | 豆瓣评分 {{ rating['average'] }} / 10
42 | {{ rating['numRaters'] }} 人评价 43 | {% end %} 44 |

45 |

46 | 查看详情 47 |

48 |
49 |
50 | {% else %} 51 |

电影可能已被删除

52 | {% end %} -------------------------------------------------------------------------------- /src/service/views/modules/music.html: -------------------------------------------------------------------------------- 1 | 2 | {% import ast %} 3 | 4 | {% if music %} 5 |
6 |
7 |

8 | 9 |

10 |
11 |
12 |
13 |

14 | {{ music.title }} 15 | {% if music.alt_title %}({{ music.alt_title }}){% end %} 16 |

17 | {% set attrs = ast.literal_eval(music.attrs) %} 18 |

{{ music.summary[0:100] }}...

19 |
20 | {% if 'singer' in attrs and attrs['singer'] %} 21 |
表演者
22 |
{{ ' / '.join(attrs['singer']) }}
23 | {% end %} 24 | 25 | {% if 'pubdate' in attrs and attrs['pubdate'] %} 26 |
发行时间
27 |
{{ ' / '.join(attrs['pubdate']) }}
28 | {% end %} 29 |
30 |
31 |
32 |
33 |

34 | {% if music.rating %} 35 | {% set rating = ast.literal_eval(music.rating) %} 36 | 豆瓣评分 {{ rating['average'] }} / 10
37 | {{ rating['numRaters'] }} 人评价 38 | {% end %} 39 |

40 |

41 | 查看详情 42 |

43 |
44 |
45 | {% else %} 46 |

唱片可能已被删除

47 | {% end %} -------------------------------------------------------------------------------- /src/service/views/modules/note.html: -------------------------------------------------------------------------------- 1 | 2 | {% from pyquery import PyQuery %} 3 | 4 | {% if note %} 5 |
6 |

7 | {{ note.title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

16 |

{{ PyQuery(note.content).text()[:200] }}...

17 |

18 | {% if note.views_count %} 19 | {{ note.views_count }}人浏览 20 | {% end %} 21 | 22 | {% if note.comments_count %} 23 | {{ note.comments_count }}回应 24 | {% end %} 25 | {% if note.like_count %} 26 | 喜欢({{ note.like_count }}) 27 | {% end %} 28 | {% if note.rec_count %} 29 | 推荐({{ note.rec_count }}) 30 | {% end %} 31 |

32 |
33 | {% else %} 34 |

日记可能已被删除

35 | {% end %} -------------------------------------------------------------------------------- /src/service/views/modules/recommend.html: -------------------------------------------------------------------------------- 1 | {% from pyquery import PyQuery %} 2 | 3 | {% if recommend %} 4 |
5 | {% set rec_img = recommend('.pic img').eq(0) %} 6 | {% if rec_img %} 7 |
8 |

9 | 10 |

11 |
12 | {% end %} 13 |
14 |
15 | {% set rec_title = recommend('.content>.title>a') %} 16 |

{{ rec_title.text() }}

17 | 18 | {% set rec_abstract = recommend('.content>p').eq(0) %} 19 |

{{ rec_abstract.text() }}

20 |
21 |
22 |
23 | {% if broadcast.object_kind == '1015' %} 24 | 查看日记 25 | {% elif broadcast.object_kind == '1001' %} 26 | 查看条目 27 | {% elif broadcast.object_kind == '1002' %} 28 | 查看条目 29 | {% elif broadcast.object_kind == '1003' %} 30 | 查看条目 31 | {% end %} 32 |
33 |
34 | {% else %} 35 |
{{ PyQuery(broadcast.content).text() }}
36 | {% end %} -------------------------------------------------------------------------------- /src/service/views/modules/user.html: -------------------------------------------------------------------------------- 1 | {% if user %} 2 |
3 |
4 |

5 | 6 |

7 |
8 |
9 |
10 |

11 | {{ user.name }} 12 | (@{{ user.unique_name}}) 13 | {{ user.signature }} 14 |

15 |
16 | {% if user.created %} 17 |
加入时间
18 |
{{ user.created }}
19 | {% end %} 20 | {% if user.loc_name %} 21 |
所在地
22 |
{{ user.loc_name }}({{ user.loc_id }})
23 | {% end %} 24 |
25 |

26 | {% if user.desc %} 27 | {% for ln in user.desc.split("\n") %} 28 | {% if ln %} 29 | {{ ln }}
30 | {% end %} 31 | {% end %} 32 | {% end %} 33 |

34 |
35 |
36 |
37 | 38 | {% else %} 39 |

用户可能已注销

40 | {% end %} -------------------------------------------------------------------------------- /src/service/views/movie.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{ subject.title }}{% end %} 4 | 5 | 6 | {% block main %} 7 | {% import ast %} 8 |
9 | 26 | {% set attrs = ast.literal_eval(subject.attrs) %} 27 |
28 |
29 |

30 | 31 |

32 |
33 |
34 |
35 |

36 | {{ subject.title }} 37 | {% if subject.alt_title %} 38 | ({{ subject.alt_title }}) 39 | {% end %} 40 |

41 |
42 | {% if 'director' in attrs and attrs['director'] %} 43 |
导演
44 |
{{ ' / '.join(attrs['director']) }}
45 | {% end %} 46 | 47 | {% if 'writer' in attrs and attrs['writer'] %} 48 |
编剧
49 |
{{ ' / '.join(attrs['writer']) }}
50 | {% end %} 51 | 52 | {% if 'cast' in attrs and attrs['cast'] %} 53 |
主演
54 |
{{ ' / '.join(attrs['cast']) }}
55 | {% end %} 56 | 57 | {% if 'movie_type' in attrs and attrs['movie_type'] %} 58 |
类型
59 |
{{ ' / '.join(attrs['movie_type']) }}
60 | {% end %} 61 | 62 | {% if 'country' in attrs and attrs['country'] %} 63 |
国家地区
64 |
{{ ' / '.join(attrs['country']) }}
65 | {% end %} 66 | 67 | {% if 'language' in attrs and attrs['language'] %} 68 |
语言
69 |
{{ ' / '.join(attrs['language']) }}
70 | {% end %} 71 | 72 | {% if 'pubdate' in attrs and attrs['pubdate'] %} 73 |
首播
74 |
{{ ' / '.join(attrs['pubdate']) }}
75 | {% end %} 76 | 77 | {% if 'episodes' in attrs and attrs['episodes'] %} 78 |
集数
79 |
{{ ' / '.join(attrs['episodes']) }}
80 | {% end %} 81 | 82 | {% if 'movie_duration' in attrs and attrs['movie_duration'] %} 83 |
单集片长
84 |
{{ ' / '.join(attrs['movie_duration']) }}
85 | {% end %} 86 |
87 | {% if mine %} 88 |
89 |
我的评价
90 |
91 | {% if mine.rating %} 92 | {% set my_rating = ast.literal_eval(mine.rating) %} 93 | 94 | {% else %} 95 | 96 | {% end %} 97 | {{ mine.create_time }} 98 |
99 | {% if mine.tags %} 100 | {% set tags = ast.literal_eval(mine.tags) %} 101 | {% if len(tags) %} 102 |
标签
103 |
{{ ' / '.join(tags) }}
104 | {% end %} 105 | {% end %} 106 |
107 |

{{ mine.comment }}

108 | {% end %} 109 |
110 |
111 |
112 |

113 | {% if subject.rating %} 114 | {% set rating = ast.literal_eval(subject.rating) %} 115 | 豆瓣评分 {{ rating['average'] }} / 10
116 | {{ rating['numRaters'] }} 人评价 117 | {% end %} 118 |

119 |
120 |
121 | 122 |
123 |

剧情简介

124 | {% for ln in subject.summary.split("\n") %} 125 | {% if ln %} 126 |

{{ ln }}

127 | {% end %} 128 | {% end %} 129 | 130 | {% if subject.tags %} 131 | {% set tags = ast.literal_eval(subject.tags) %} 132 |

豆瓣成员常用的标签

133 | {% for tag in tags %} 134 | {{ tag['name'] }}({{ tag['count'] }}) 135 | {% end %} 136 | {% end %} 137 |
138 |
139 | {% end %} -------------------------------------------------------------------------------- /src/service/views/music.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{ subject.title }}{% end %} 4 | 5 | {% block main %} 6 | {% import ast %} 7 |
8 | 25 | {% set attrs = ast.literal_eval(subject.attrs) %} 26 |
27 |
28 |

29 | 30 |

31 |
32 |
33 |
34 |

35 | {{ subject.title }} 36 | {% if subject.alt_title %} 37 |
({{ subject.alt_title }}) 38 | {% end %} 39 |

40 |
41 | {% if 'singer' in attrs and attrs['singer'] %} 42 |
表演者
43 |
{{ ' / '.join(attrs['singer']) }}
44 | {% end %} 45 | 46 | {% if 'version' in attrs and attrs['version'] %} 47 |
专辑类型
48 |
{{ ' / '.join(attrs['version']) }}
49 | {% end %} 50 | 51 | {% if 'media' in attrs and attrs['media'] %} 52 |
介质
53 |
{{ ' / '.join(attrs['media']) }}
54 | {% end %} 55 | 56 | {% if 'pubdate' in attrs and attrs['pubdate'] %} 57 |
发行时间
58 |
{{ ' / '.join(attrs['pubdate']) }}
59 | {% end %} 60 | 61 | {% if 'publisher' in attrs and attrs['publisher'] %} 62 |
出版者
63 |
{{ ' / '.join(attrs['publisher']) }}
64 | {% end %} 65 | 66 | {% if 'discs' in attrs and attrs['discs'] %} 67 |
唱片数
68 |
{{ ' / '.join(attrs['discs']) }}
69 | {% end %} 70 |
71 |
72 |
我的评价
73 |
74 | {% if mine.rating %} 75 | {% set my_rating = ast.literal_eval(mine.rating) %} 76 | 77 | {% else %} 78 | 79 | {% end %} 80 | {{ mine.create_time }} 81 |
82 | {% if mine.tags %} 83 | {% set tags = ast.literal_eval(mine.tags) %} 84 | {% if len(tags) %} 85 |
标签
86 |
{{ ' / '.join(tags) }}
87 | {% end %} 88 | {% end %} 89 |
90 |

{{ mine.comment }}

91 |
92 |
93 |
94 |

95 | {% if subject.rating %} 96 | {% set rating = ast.literal_eval(subject.rating) %} 97 | 豆瓣评分 {{ rating['average'] }} / 10
98 | {{ rating['numRaters'] }} 人评价 99 | {% end %} 100 |

101 |
102 |
103 | 104 |
105 | {% if subject.summary %} 106 |

简介

107 | {% for ln in subject.summary.split("\n") %} 108 | {% if ln %} 109 |

{{ ln }}

110 | {% end %} 111 | {% end %} 112 | {% end %} 113 | 114 | {% if 'tracks' in attrs %} 115 |

曲目

116 |

117 | {% for ln in attrs['tracks'] %} 118 | {% for subln in ln.split("\n") %} 119 | {% if subln %}{{ subln }}
{% end %} 120 | {% end %}
121 | {% end %} 122 |

123 | {% end %} 124 | 125 | {% if subject.tags %} 126 | {% set tags = ast.literal_eval(subject.tags) %} 127 |

豆瓣成员常用的标签

128 | {% for tag in tags %} 129 | {{ tag['name'] }}({{ tag['count'] }}) 130 | {% end %} 131 | {% end %} 132 |
133 | 134 |
135 | {% end %} 136 | -------------------------------------------------------------------------------- /src/service/views/my/_menu.html: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /src/service/views/my/blocklist.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}我的黑名单{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 |
9 |
10 |
11 | {% module Template('my/_menu.html', active_page='blocklist') %} 12 |
13 |
14 | {% block headline %} 15 | 30 | {% end %} 31 | 32 | {% try %}{% if search %} 33 | 43 | {% end %}{% except NameError %}{% end %} 44 | 45 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 46 | {% for row in rows %} 47 |
48 |
49 |

50 | 51 |

52 |
53 |
54 |
55 |

56 | {{ row.block_user.name }} 57 | (@{{ row.block_user.unique_name}}) 58 | {{ row.block_user.signature }} 59 |

60 |
61 | {% if row.block_user.created %} 62 |
加入时间
63 |
{{ row.block_user.created }}
64 | {% end %} 65 | {% if row.block_user.loc_name %} 66 |
所在地
67 |
{{ row.block_user.loc_name }}({{ row.block_user.loc_id }})
68 | {% end %} 69 |
70 |

71 | {% if row.block_user.desc %} 72 | {% for ln in row.block_user.desc.split("\n") %} 73 | {% if ln %} 74 | {{ ln }}
75 | {% end %} 76 | {% end %} 77 | {% end %} 78 |

79 |
80 |
81 |
82 | {% if row.block_user.version > 1 %} 83 | 历史记录 84 | {% end if %} 85 |
86 |
87 | {% end %} 88 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 89 |
90 | 91 |
92 | 93 |
94 | {% end %} 95 | -------------------------------------------------------------------------------- /src/service/views/my/blocklist_historical.html: -------------------------------------------------------------------------------- 1 | {% extends "blocklist.html" %} 2 | 3 | {% block title %}我的黑名单历史记录{% end %} 4 | 5 | {% block headline %} 6 | 18 | {% end %} -------------------------------------------------------------------------------- /src/service/views/my/book.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}{{ '我读过的书' if status == 'done' else '我想读的书' if status == 'wish' else '我在读的书' }}{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 | {% import ast %} 9 |
10 |
11 |
12 | {% module Template('my/_menu.html', active_page='book/' + status) %} 13 |
14 |
15 | {% block headline %} 16 | 31 | {% end %} 32 | 33 | {% try %}{% if search %} 34 | 44 | {% end %}{% except NameError %}{% end %} 45 | 46 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 47 | {% for row in rows %} 48 |
49 |
50 |

51 | 52 |

53 |
54 |
55 |
56 |

57 | {{ row.book.title }} 58 | {{ row.book.subtitle }} 59 | {% if row.book.alt_title %} 60 |
({{ row.book.alt_title }}) 61 | {% end %} 62 |

63 |
64 | {% if row.book.author %} 65 | {% set author = ast.literal_eval(row.book.author) %} 66 | {% if len(author) %} 67 |
作者
68 |
{{ ' / '.join(author) }}
69 | {% end %} 70 | {% end %} 71 | 72 | {% if row.book.translator %} 73 | {% set translator = ast.literal_eval(row.book.translator) %} 74 | {% if len(translator) %} 75 |
译者
76 |
{{ ' / '.join(translator) }}
77 | {% end %} 78 | {% end %} 79 | 80 | {% if row.book.publisher %} 81 |
出版社
82 |
{{ row.book.publisher }}
83 | {% end %} 84 | 85 | {% if row.book.origin_title %} 86 |
原作名
87 |
{{ row.book.origin_title }}
88 | {% end %} 89 | 90 | {% if row.book.pubdate %} 91 |
出版日期
92 |
{{ row.book.pubdate }}
93 | {% end %} 94 | 95 | {% if row.book.isbn10 or row.book.isbn13 %} 96 |
ISBN
97 |
{{ row.book.isbn10 }} / {{ row.book.isbn13 }}
98 | {% end %} 99 | 100 | {% if row.book.price %} 101 |
价格
102 |
{{ row.book.price }}
103 | {% end %} 104 | 105 | {% if row.book.pages %} 106 |
页数
107 |
{{ row.book.pages }}
108 | {% end %} 109 | 110 | {% if row.book.binding %} 111 |
装帧
112 |
{{ row.book.binding }}
113 | {% end %} 114 |
115 |
116 |
我的评价
117 |
118 | {% if row.rating %} 119 | {% set my_rating = ast.literal_eval(row.rating) %} 120 | 121 | {% else %} 122 | 123 | {% end %} 124 | {{ row.create_time }} 125 |
126 | {% if row.tags %} 127 | {% set tags = ast.literal_eval(row.tags) %} 128 | {% if len(tags) %} 129 |
标签
130 |
{{ ' / '.join(tags) }}
131 | {% end %} 132 | {% end %} 133 |
134 |

{{ row.comment }}

135 |
136 |
137 |
138 |

139 | {% if row.book.rating %} 140 | {% set rating = ast.literal_eval(row.book.rating) %} 141 | 豆瓣评分 {{ rating['average'] }} / 10
142 | {{ rating['numRaters'] }} 人评价 143 | {% end %} 144 |

145 |

146 | 查看详情 147 |

148 |
149 |
150 | {% end %} 151 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 152 |
153 | 154 |
155 | 156 |
157 | {% end %} 158 | -------------------------------------------------------------------------------- /src/service/views/my/book_historical.html: -------------------------------------------------------------------------------- 1 | {% extends "book.html" %} 2 | 3 | 4 | {% block title %}读书的历史记录{% end %} 5 | 6 | 7 | {% block headline %} 8 | 20 | {% end %} -------------------------------------------------------------------------------- /src/service/views/my/broadcast.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}我的广播{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 | {% import ast %} 9 | 10 | {% from pyquery import PyQuery %} 11 |
12 |
13 |
14 | {% module Template('my/_menu.html', active_page='broadcast') %} 15 |
16 |
17 | {% block headline %} 18 | 30 | {% end %} 31 | 32 | {% try %}{% if search %} 33 | 43 | {% end %}{% except NameError %}{% end %} 44 | 45 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 46 | {% for row in rows %} 47 | {% module Template('my/_broadcast.html', boxed=True, wrapped=False, broadcast=row.broadcast) %} 48 | {% end %} 49 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 50 |
51 | 52 |
53 | 54 |
55 | 56 | 65 | {% end %} 66 | 67 | {% block body_extra %} 68 | 94 | {% end %} -------------------------------------------------------------------------------- /src/service/views/my/favorite/note.html: -------------------------------------------------------------------------------- 1 | {% extends "../my.html" %} 2 | 3 | {% block title %}我喜欢的日记{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 | {% from pyquery import PyQuery %} 9 | 10 | 11 |
12 |
13 |
14 | {% module Template('my/_menu.html', active_page='favorite') %} 15 |
16 |
17 |
18 | 22 |
23 | {% block headline %} 24 | 36 | {% end %} 37 | 38 | {% try %}{% if search %} 39 | 49 | {% end %}{% except NameError %}{% end %} 50 | 51 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 52 | {% for favorite in rows %} 53 | {% set row = favorite.note %} 54 |
55 |

56 | {{ row.title }} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |

65 |

66 | {{ row.created }} 67 | {% try %} 68 | {% if row.user_id > 0 and row.user %} 69 | 作者:{{ row.user.name }} 70 | {% end %} 71 | {% except %} 72 | {% end %} 73 |

74 |

75 | {{ PyQuery(row.content).text()[:200] }}... 76 |

77 |

78 | {% if row.views_count %} 79 | {{ row.views_count }}人浏览 80 | {% end %} 81 | 82 | {% if row.comments_count %} 83 | {{ row.comments_count }}回应 84 | {% end %} 85 | {% if row.like_count %} 86 | 喜欢({{ row.like_count }}) 87 | {% end %} 88 | {% if row.rec_count %} 89 | 推荐({{ row.rec_count }}) 90 | {% end %} 91 |

92 |
93 | {% end %} 94 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 95 |
96 | 97 |
98 | 99 |
100 | {% end %} 101 | -------------------------------------------------------------------------------- /src/service/views/my/favorite/photo.html: -------------------------------------------------------------------------------- 1 | {% extends "../my.html" %} 2 | 3 | {% block title %}我的相册{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 |
9 |
10 |
11 | {% module Template('my/_menu.html', active_page='favorite') %} 12 |
13 |
14 |
15 | 19 |
20 | {% block headline %} 21 | 33 | {% end %} 34 | 35 | {% try %}{% if search %} 36 | 46 | {% end %}{% except NameError %}{% end %} 47 | 48 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 49 | {% for favorite in rows %} 50 | {% set row = favorite.photo_album %} 51 |
52 |
53 |

54 | 55 | {% if row.cover %} 56 | 57 | {% else %} 58 | 59 | {% end %} 60 | 61 |

62 |
63 |
64 |
65 |

66 | {{ row.title }} 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |

75 | {% if row.last_updated %} 76 |

77 | {{ row.last_updated }} 78 |

79 | {% end %} 80 |

81 | {{ row.desc }} 82 |

83 |

84 | {{ row.photos_count }}张照片 85 | {% if row.views_count %} 86 | {{ row.views_count }}人浏览 87 | {% end %} 88 | {% if row.like_count %} 89 | 喜欢({{ row.like_count }}) 90 | {% end %} 91 | {% if row.rec_count %} 92 | 推荐({{ row.rec_count }}) 93 | {% end %} 94 |

95 |
96 |
97 |
98 | {% end %} 99 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 100 |
101 | 102 |
103 | 104 |
105 | {% end %} 106 | -------------------------------------------------------------------------------- /src/service/views/my/followers.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}关注我的{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 |
9 |
10 |
11 | {% module Template('my/_menu.html', active_page='followers') %} 12 |
13 |
14 | {% block headline %} 15 | 30 | {% end %} 31 | 32 | {% try %}{% if search %} 33 | 43 | {% end %}{% except NameError %}{% end %} 44 | 45 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 46 | {% for row in rows %} 47 |
48 |
49 |

50 | 51 |

52 |
53 |
54 |
55 |

56 | {{ row.follower.name }} 57 | (@{{ row.follower.unique_name}}) 58 | {{ row.follower.signature }} 59 |

60 |
61 | {% if row.follower.created %} 62 |
加入时间
63 |
{{ row.follower.created }}
64 | {% end %} 65 | {% if row.follower.loc_name %} 66 |
所在地
67 |
{{ row.follower.loc_name }}({{ row.follower.loc_id }})
68 | {% end %} 69 |
70 |

71 | {% if row.follower.desc %} 72 | {% for ln in row.follower.desc.split("\n") %} 73 | {% if ln %} 74 | {{ ln }}
75 | {% end %} 76 | {% end %} 77 | {% end %} 78 |

79 |
80 |
81 |
82 | {% if row.follower.version > 1 %} 83 | 历史记录 84 | {% end if %} 85 |
86 |
87 | {% end %} 88 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 89 |
90 | 91 |
92 | 93 |
94 | {% end %} 95 | -------------------------------------------------------------------------------- /src/service/views/my/followers_historical.html: -------------------------------------------------------------------------------- 1 | {% extends "followers.html" %} 2 | 3 | {% block title %}关注我的历史记录{% end %} 4 | 5 | {% block headline %} 6 | 18 | {% end %} -------------------------------------------------------------------------------- /src/service/views/my/following.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}我关注的{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 |
9 |
10 |
11 | {% module Template('my/_menu.html', active_page='following') %} 12 |
13 |
14 | {% block headline %} 15 | 30 | {% end %} 31 | 32 | {% try %}{% if search %} 33 | 43 | {% end %}{% except NameError %}{% end %} 44 | 45 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 46 | {% for row in rows %} 47 |
48 |
49 |

50 | 51 |

52 |
53 |
54 |
55 |

56 | {{ row.following_user.name }} 57 | (@{{ row.following_user.unique_name}}) 58 | {{ row.following_user.signature }} 59 |

60 |
61 | {% if row.following_user.created %} 62 |
加入时间
63 |
{{ row.following_user.created }}
64 | {% end %} 65 | {% if row.following_user.loc_name %} 66 |
所在地
67 |
{{ row.following_user.loc_name }}({{ row.following_user.loc_id }})
68 | {% end %} 69 |
70 |

71 | {% if row.following_user.desc %} 72 | {% for ln in row.following_user.desc.split("\n") %} 73 | {% if ln %} 74 | {{ ln }}
75 | {% end %} 76 | {% end %} 77 | {% end %} 78 |

79 |
80 |
81 |
82 | {% if row.following_user.version > 1 %} 83 | 历史记录 84 | {% end if %} 85 |
86 |
87 | {% end %} 88 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 89 |
90 | 91 |
92 | 93 |
94 | {% end %} 95 | -------------------------------------------------------------------------------- /src/service/views/my/following_historical.html: -------------------------------------------------------------------------------- 1 | {% extends "following.html" %} 2 | 3 | {% block title %}我关注的历史记录{% end %} 4 | 5 | {% block headline %} 6 | 18 | {% end %} -------------------------------------------------------------------------------- /src/service/views/my/movie.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}{{ '我看过的影视' if status == 'done' else '我想看的影视' if status == 'wish' else '我在看的影视' }}{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 | {% import ast %} 9 |
10 |
11 |
12 | {% module Template('my/_menu.html', active_page='movie/' + status) %} 13 |
14 |
15 | {% block headline %} 16 | 31 | {% end %} 32 | 33 | {% try %}{% if search %} 34 | 44 | {% end %}{% except NameError %}{% end %} 45 | 46 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 47 | {% for row in rows %} 48 | {% set attrs = ast.literal_eval(row.movie.attrs) %} 49 |
50 |
51 |

52 | 53 |

54 |
55 |
56 |
57 |

58 | {{ row.movie.title }} 59 | {% if row.movie.alt_title %} 60 | ({{ row.movie.alt_title }}) 61 | {% end %} 62 |

63 |
64 | {% if 'director' in attrs and attrs['director'] %} 65 |
导演
66 |
{{ ' / '.join(attrs['director']) }}
67 | {% end %} 68 | 69 | {% if 'writer' in attrs and attrs['writer'] %} 70 |
编剧
71 |
{{ ' / '.join(attrs['writer']) }}
72 | {% end %} 73 | 74 | {% if 'cast' in attrs and attrs['cast'] %} 75 |
主演
76 |
{{ ' / '.join(attrs['cast']) }}
77 | {% end %} 78 | 79 | {% if 'movie_type' in attrs and attrs['movie_type'] %} 80 |
类型
81 |
{{ ' / '.join(attrs['movie_type']) }}
82 | {% end %} 83 | 84 | {% if 'country' in attrs and attrs['country'] %} 85 |
国家地区
86 |
{{ ' / '.join(attrs['country']) }}
87 | {% end %} 88 | 89 | {% if 'language' in attrs and attrs['language'] %} 90 |
语言
91 |
{{ ' / '.join(attrs['language']) }}
92 | {% end %} 93 | 94 | {% if 'pubdate' in attrs and attrs['pubdate'] %} 95 |
首播
96 |
{{ ' / '.join(attrs['pubdate']) }}
97 | {% end %} 98 | 99 | {% if 'episodes' in attrs and attrs['episodes'] %} 100 |
集数
101 |
{{ ' / '.join(attrs['episodes']) }}
102 | {% end %} 103 | 104 | {% if 'movie_duration' in attrs and attrs['movie_duration'] %} 105 |
单集片长
106 |
{{ ' / '.join(attrs['movie_duration']) }}
107 | {% end %} 108 |
109 |
110 |
我的评价
111 |
112 | {% if row.rating %} 113 | {% set my_rating = ast.literal_eval(row.rating) %} 114 | 115 | {% else %} 116 | 117 | {% end %} 118 | {{ row.create_time }} 119 |
120 | {% if row.tags %} 121 | {% set tags = ast.literal_eval(row.tags) %} 122 | {% if len(tags) %} 123 |
标签
124 |
{{ ' / '.join(tags) }}
125 | {% end %} 126 | {% end %} 127 |
128 |

{{ row.comment }}

129 |
130 |
131 |
132 |

133 | {% if row.movie.rating %} 134 | {% set rating = ast.literal_eval(row.movie.rating) %} 135 | 豆瓣评分 {{ rating['average'] }} / 10
136 | {{ rating['numRaters'] }} 人评价 137 | {% end %} 138 |

139 |

140 | 查看详情 141 |

142 |
143 |
144 | {% end %} 145 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 146 |
147 | 148 |
149 | 150 |
151 | {% end %} -------------------------------------------------------------------------------- /src/service/views/my/music.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}{{ '我听过的音乐' if status == 'done' else '我想听的音乐' if status == 'wish' else '我在听的音乐' }}{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 | {% import ast %} 9 |
10 |
11 |
12 | {% module Template('my/_menu.html', active_page='music/' + status) %} 13 |
14 |
15 | {% block headline %} 16 | 31 | {% end %} 32 | 33 | {% try %}{% if search %} 34 | 44 | {% end %}{% except NameError %}{% end %} 45 | 46 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 47 | {% for row in rows %} 48 |
49 | {% set attrs = ast.literal_eval(row.music.attrs) %} 50 |
51 |

52 | 53 |

54 |
55 |
56 |
57 |

58 | {{ row.music.title }} 59 | {% if row.music.alt_title %} 60 |
({{ row.music.alt_title }}) 61 | {% end %} 62 |

63 |
64 | {% if 'singer' in attrs and attrs['singer'] %} 65 |
表演者
66 |
{{ ' / '.join(attrs['singer']) }}
67 | {% end %} 68 | 69 | {% if 'version' in attrs and attrs['version'] %} 70 |
专辑类型
71 |
{{ ' / '.join(attrs['version']) }}
72 | {% end %} 73 | 74 | {% if 'media' in attrs and attrs['media'] %} 75 |
介质
76 |
{{ ' / '.join(attrs['media']) }}
77 | {% end %} 78 | 79 | {% if 'pubdate' in attrs and attrs['pubdate'] %} 80 |
发行时间
81 |
{{ ' / '.join(attrs['pubdate']) }}
82 | {% end %} 83 | 84 | {% if 'publisher' in attrs and attrs['publisher'] %} 85 |
出版者
86 |
{{ ' / '.join(attrs['publisher']) }}
87 | {% end %} 88 | 89 | {% if 'discs' in attrs and attrs['discs'] %} 90 |
唱片数
91 |
{{ ' / '.join(attrs['discs']) }}
92 | {% end %} 93 |
94 |
95 |
我的评价
96 |
97 | {% if row.rating %} 98 | {% set my_rating = ast.literal_eval(row.rating) %} 99 | 100 | {% else %} 101 | 102 | {% end %} 103 | {{ row.create_time }} 104 |
105 | {% if row.tags %} 106 | {% set tags = ast.literal_eval(row.tags) %} 107 | {% if len(tags) %} 108 |
标签
109 |
{{ ' / '.join(tags) }}
110 | {% end %} 111 | {% end %} 112 |
113 |

{{ row.comment }}

114 |
115 |
116 |
117 |

118 | {% if row.music.rating %} 119 | {% set rating = ast.literal_eval(row.music.rating) %} 120 | 豆瓣评分 {{ rating['average'] }} / 10
121 | {{ rating['numRaters'] }} 人评价 122 | {% end %} 123 |

124 |

125 | 查看详情 126 |

127 |
128 |
129 | {% end %} 130 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 131 |
132 | 133 |
134 | 135 |
136 | {% end %} 137 | -------------------------------------------------------------------------------- /src/service/views/my/music_historical.html: -------------------------------------------------------------------------------- 1 | {% extends "music.html" %} 2 | 3 | {% block title %}听音乐的历史记录{% end %} 4 | 5 | {% block headline %} 6 | 18 | {% end %} -------------------------------------------------------------------------------- /src/service/views/my/my.html: -------------------------------------------------------------------------------- 1 | {% extends "../themes/main.html" %} 2 | 3 | {% block search %} 4 |
5 | 15 |
16 | {% end %} 17 | 18 | {% block body_extra %} 19 | 40 | {% end %} 41 | -------------------------------------------------------------------------------- /src/service/views/my/note.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}我的日记{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 | {% from pyquery import PyQuery %} 9 |
10 |
11 |
12 | {% module Template('my/_menu.html', active_page='note') %} 13 |
14 |
15 | {% block headline %} 16 | 28 | {% end %} 29 | 30 | {% try %}{% if search %} 31 | 41 | {% end %}{% except NameError %}{% end %} 42 | 43 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 44 | {% for row in rows %} 45 |
46 |

47 | {{ row.title }} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |

56 |

57 | {{ row.created }} 58 |

59 |

60 | {{ PyQuery(row.content).text()[:200] }}... 61 |

62 |

63 | {% if row.views_count %} 64 | {{ row.views_count }}人浏览 65 | {% end %} 66 | 67 | {% if row.comments_count %} 68 | {{ row.comments_count }}回应 69 | {% end %} 70 | {% if row.like_count %} 71 | 喜欢({{ row.like_count }}) 72 | {% end %} 73 | {% if row.rec_count %} 74 | 推荐({{ row.rec_count }}) 75 | {% end %} 76 |

77 |
78 | {% end %} 79 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 80 |
81 | 82 |
83 | 84 |
85 | {% end %} 86 | -------------------------------------------------------------------------------- /src/service/views/my/photo.html: -------------------------------------------------------------------------------- 1 | {% extends "my.html" %} 2 | 3 | {% block title %}我的相册{% end %} 4 | 5 | {% block search_key %}{% try %}{{ search if search else '' }}{% except NameError %}{% end %}{% end %} 6 | 7 | {% block main %} 8 |
9 |
10 |
11 | {% module Template('my/_menu.html', active_page='photo') %} 12 |
13 |
14 | {% block headline %} 15 | 27 | {% end %} 28 | 29 | {% try %}{% if search %} 30 | 40 | {% end %}{% except NameError %}{% end %} 41 | 42 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 43 | {% for row in rows %} 44 |
45 |
46 |

47 | 48 | {% if row.cover %} 49 | 50 | {% else %} 51 | 52 | {% end %} 53 | 54 |

55 |
56 |
57 |
58 |

59 | {{ row.title }} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |

68 |

69 | {{ row.last_updated }} 70 |

71 |

72 | {{ row.desc }} 73 |

74 |

75 | {{ row.photos_count }}张照片 76 | {% if row.views_count %} 77 | {{ row.views_count }}人浏览 78 | {% end %} 79 | {% if row.like_count %} 80 | 喜欢({{ row.like_count }}) 81 | {% end %} 82 | {% if row.rec_count %} 83 | 推荐({{ row.rec_count }}) 84 | {% end %} 85 |

86 |
87 |
88 |
89 | {% end %} 90 | {% module Template('themes/paginator.html', page=page, total_pages=total_pages, page_capacity=10) %} 91 |
92 | 93 |
94 | 95 |
96 | {% end %} 97 | -------------------------------------------------------------------------------- /src/service/views/note.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{ note.title }}{% end %} 4 | 5 | {% block head_extra %} 6 | 7 | {% end %} 8 | 9 | {% block main %} 10 | {% from pyquery import PyQuery %} 11 |
12 | 30 | 31 |
32 |

33 | {{ note.title }} 34 |

35 |

36 | {{ note.created }} 37 | {% try %} 38 | {% if note.user_id > 0 and note.user %} 39 | 作者:{{ note.user.name }} 40 | {% end %} 41 | {% except %} 42 | {% end %} 43 |

44 |
45 | {%raw content %} 46 |
47 | 51 |

52 | {% if note.views_count %} 53 | {{ note.views_count }}人浏览 54 | {% end %} 55 | 56 | {% if note.comments_count %} 57 | {{ note.comments_count }}回应 58 | {% end %} 59 | {% if note.like_count %} 60 | 喜欢({{ note.like_count }}) 61 | {% end %} 62 | {% if note.rec_count %} 63 | 推荐({{ note.rec_count }}) 64 | {% end %} 65 |

66 |
67 | 68 | {% for comment in comments %} 69 |
70 |
71 |

72 | 73 |

74 |
75 |
76 |

77 | {{ comment.user.name }} 78 | (@{{ comment.user.unique_name}}) 79 | {{ comment.user.signature }} 80 | {{ comment.created}} 81 |

82 |

{{ comment.text }}

83 |
84 |
85 | {% end %} 86 | 87 |
88 | {% end %} 89 | 90 | {% block body_extra %} 91 | 128 | {% end %} -------------------------------------------------------------------------------- /src/service/views/photo.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{ photo.photo_album.title }}{% end %} 4 | 5 | {% block head_extra %} 6 | 7 | {% end %} 8 | 9 | {% block main %} 10 | {% from pyquery import PyQuery %} 11 |
12 | 29 | 30 |
31 |

32 | {{ photo.photo_album.title }} 33 |

34 |
35 | 36 |
37 |

{{ photo.desc }}

38 |

39 | {% if photo.views_count %} 40 | {{ photo.views_count }}人浏览 41 | {% end %} 42 | {% if photo.comments_count %} 43 | {{ photo.comments_count }}回应 44 | {% end %} 45 | {% if photo.like_count %} 46 | 喜欢({{ photo.like_count }}) 47 | {% end %} 48 | {% if photo.rec_count %} 49 | 推荐({{ photo.rec_count }}) 50 | {% end %} 51 |

52 |
53 | 54 | {% for comment in comments %} 55 |
56 |
57 |

58 | 59 |

60 |
61 |
62 |

63 | {{ comment.user.name }} 64 | (@{{ comment.user.unique_name}}) 65 | {{ comment.user.signature }} 66 | {{ comment.created}} 67 |

68 |

{{ comment.text }}

69 |
70 |
71 | {% end %} 72 | 73 |
74 | {% end %} 75 | -------------------------------------------------------------------------------- /src/service/views/search.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/base.html" %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 | {% end %} -------------------------------------------------------------------------------- /src/service/views/settings/_menu.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/service/views/settings/accounts/index.html: -------------------------------------------------------------------------------- 1 | {% extends "../../themes/main.html" %} 2 | 3 | {% block title %}设置{% end %} 4 | 5 | {% block main %} 6 |
7 | 17 | 18 |
19 |
20 | {% module Template('settings/_menu.html', active_page='account') %} 21 |
22 |
23 | {% if len(rows) == 0 %} 24 |
没有帐号,请先登录
25 | {% else %} 26 | {% for row in rows %} 27 |
28 |
29 |

30 | 31 |

32 | {% if not row.is_activated %} 33 |

切换

34 | {% end %} 35 |

备份

36 |
37 |
38 |
39 | 57 |
58 |
59 |
60 | 61 |
62 |
63 | {% end %} 64 |

添加

65 | {% end %} 66 |
67 |
68 | 69 |
70 | {% end %} 71 | 72 | {% block body_extra %} 73 | 101 | {% end %} -------------------------------------------------------------------------------- /src/service/views/settings/accounts/login.html: -------------------------------------------------------------------------------- 1 | {% extends "../../themes/main.html" %} 2 | 3 | {% block title %}登录{% end %} 4 | 5 | {% block head_extra %} 6 | 12 | {% end %} 13 | 14 | {% block main %} 15 |
16 | 32 | 33 |
34 |
程序使用豆瓣官方网页登录,不会获取和保存你的密码。为了保障你的帐号安全,请从官方渠道下载本软件。
35 |
36 | 37 |
38 |

39 | 40 |

41 |

42 | 43 | 44 | 45 | 46 | 刷新 47 | 48 |

49 |

50 | 51 | 52 | 53 | 54 | 返回登录页 55 | 56 |

57 |
58 | 59 | {% import time %} 60 | 61 |
62 | {% end %} 63 | 64 | {% block body_extra %} 65 | 118 | {% end %} 119 | -------------------------------------------------------------------------------- /src/service/views/settings/general.html: -------------------------------------------------------------------------------- 1 | {% extends "settings.html" %} 2 | 3 | {% block setting_menu %} 4 | {% module Template('settings/_menu.html', active_page='general') %} 5 | {% end %} 6 | 7 | {% block setting_content %} 8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |

17 | 18 |

19 |

20 | 次/分钟 21 |

22 |
23 |

豆瓣网站对单个IP发起的请求频率有限制。如果数字设置过大,可能导致IP被豆瓣封杀。

24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |

36 | 37 |

38 |

39 | 40 |

41 |
42 |

本地数据(包括书、影、音、用户)在超过这个周期后,会尝试从网上获取最新的数据。

43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 |
53 |
54 |

55 | 56 |

57 |

58 | 59 |

60 |
61 |

只更新活跃期内的广播回应。早于活跃期范围的广播评论将不再更新。

62 |
63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 | {% if broadcast_incremental_backup %} 75 | 79 | 83 | {% else %} 84 | 88 | 92 | {% end %} 93 |
94 |
95 |

只获取并备份新增的广播。

96 |
97 |
98 |
99 | 100 |
101 |
102 | 103 |
104 |
105 |
106 |
107 |
108 | {% if image_local_cache %} 109 | 113 | 117 | {% else %} 118 | 122 | 126 | {% end %} 127 |
128 |
129 |

将广播、相册、日记中的图片下载到本地。

130 |
131 |
132 |
133 | 134 |
135 |
136 | 137 |
138 |
139 | 140 |
141 |
142 |
143 | 144 | {% end %} -------------------------------------------------------------------------------- /src/service/views/settings/network.html: -------------------------------------------------------------------------------- 1 | {% extends "settings.html" %} 2 | 3 | {% block setting_menu %} 4 | {% module Template('settings/_menu.html', active_page='network') %} 5 | {% end %} 6 | 7 | {% block setting_content %} 8 |
9 | 10 |

11 | 12 |

13 |

每行一个代理服务器地址

14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | {% end %} -------------------------------------------------------------------------------- /src/service/views/settings/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "../themes/main.html" %} 2 | 3 | {% block title %}设置{% end %} 4 | 5 | {% block main %} 6 |
7 | 17 | 18 |
19 |
20 | {% block setting_menu %}{% end %} 21 |
22 |
23 | {% if flash %} 24 |
25 | 26 | {{ flash }} 27 |
28 | {% end %} 29 | 30 | {% block setting_content %}{% end %} 31 |
32 |
33 | 34 |
35 | {% end %} -------------------------------------------------------------------------------- /src/service/views/themes/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |
5 | {% block main %} 6 | 7 | {% end %} 8 |
9 | {% end %} -------------------------------------------------------------------------------- /src/service/views/themes/paginator.html: -------------------------------------------------------------------------------- 1 | {% if total_pages > 1 %} 2 | {% import re %} 3 | {% set paging_uri = lambda page, query: re.sub(r'(^page|(?<=&)page)=(\d+$|\d+(?=&))', 'page={0}'.format(page), query) if re.search(r'(^page|(?<=&)page)=(\d+$|\d+(?=&))', query) else query + '&page={0}'.format(page) %} 4 | 42 | {% end %} -------------------------------------------------------------------------------- /src/service/views/user.html: -------------------------------------------------------------------------------- 1 | {% extends "themes/main.html" %} 2 | 3 | {% block title %}{{ subject.name }}{% end %} 4 | 5 | {% block main %} 6 | {% import ast %} 7 |
8 | 25 | 26 |
27 |
28 |

29 | 30 |

31 |
32 |
33 |
34 |

35 | {{ subject.name }} 36 | (@{{ subject.unique_name}}) 37 | {{ subject.signature }} 38 |

39 |
40 | {% if subject.created %} 41 |
加入时间
42 |
{{ subject.created }}
43 | {% end %} 44 | {% if subject.loc_name %} 45 |
所在地
46 |
{{ subject.loc_name }}({{ subject.loc_id }})
47 | {% end %} 48 |
49 |

50 | {% if subject.desc %} 51 | {% for ln in subject.desc.split("\n") %} 52 | {% if ln %} 53 | {{ ln }}
54 | {% end %} 55 | {% end %} 56 | {% end %} 57 |

58 |
59 |
60 |
61 | {% if is_follower and is_following %} 62 | 相互关注 63 | {% elif is_follower %} 64 | 我的关注者 65 | {% elif is_following %} 66 | 已关注 67 | {% end %} 68 |
69 |
70 | 71 |
72 | {% end %} 73 | -------------------------------------------------------------------------------- /src/service/worker.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import logging 3 | import traceback 4 | from enum import Enum 5 | from inspect import isgeneratorfunction 6 | from logging.handlers import QueueHandler 7 | from multiprocessing import Process, Queue, queues 8 | 9 | import db 10 | import tasks 11 | 12 | 13 | REQUESTS_PER_MINUTE = 60 14 | LOCAL_OBJECT_DURATION = 60 * 60 * 24 * 30 15 | BROADCAST_ACTIVE_DURATION = 60 * 60 * 24 * 30 16 | BROADCAST_INCREMENTAL_BACKUP = True 17 | IMAGE_LOCAL_CACHE = True 18 | HEARTBEAT_INTERVAL = 10 19 | 20 | 21 | class Worker: 22 | """ 23 | 工作进程封装 24 | """ 25 | 26 | _id = 1 27 | _name = '工作进程' 28 | 29 | class ReturnError: 30 | """ 31 | 工作进程发生异常 32 | """ 33 | 34 | def __init__(self, name, exception, traceback): 35 | self.name = name 36 | self.exception = exception 37 | self.traceback = traceback 38 | 39 | class ReturnDone: 40 | """ 41 | 任务完成 42 | """ 43 | 44 | def __init__(self, name, value): 45 | self.name = name 46 | self.value = value 47 | 48 | class ReturnHeartbeat: 49 | """ 50 | 心跳 51 | """ 52 | 53 | def __init__(self, name, sequence): 54 | self.name = name 55 | self.sequence = sequence 56 | 57 | class ReturnReady: 58 | """ 59 | 工作进程准备完毕 60 | """ 61 | 62 | def __init__(self, name): 63 | self.name = name 64 | 65 | class ReturnWorking(): 66 | """ 67 | 接收到任务准备工作 68 | """ 69 | 70 | def __init__(self, name, task): 71 | self.name = name 72 | self.task = task 73 | 74 | class State(Enum): 75 | """ 76 | 工作进程状态 77 | """ 78 | PENDING = 0 79 | RUNNING = 1 80 | TERMINATED = 2 81 | 82 | class RuntimeError(Exception): 83 | """ 84 | 运行时错误 85 | """ 86 | def __init__(self, message): 87 | self.message = message 88 | 89 | def __init__(self, debug=False, queue_in=None, queue_out=None, **settings): 90 | class_type = type(self) 91 | self._name = '{name}#{id}'.format(name=class_type._name, id=class_type._id) 92 | class_type._id += 1 93 | 94 | self._status = Worker.State.PENDING 95 | self._queue_in = queue_in 96 | self._queue_out = queue_out 97 | self._settings = settings 98 | self._current_task = None 99 | self._debug = debug 100 | 101 | @property 102 | def queue_in(self): 103 | if self._queue_in is None: 104 | self._queue_in = Queue() 105 | return self._queue_in 106 | 107 | @property 108 | def queue_out(self): 109 | if self._queue_out is None: 110 | self._queue_out = Queue() 111 | return self._queue_out 112 | 113 | @property 114 | def name(self): 115 | return self._name 116 | 117 | def __str__(self): 118 | return self.name 119 | 120 | def _ready(self): 121 | self.queue_out.put(Worker.ReturnReady(self._name)) 122 | 123 | def _work(self, task): 124 | self.queue_out.put(Worker.ReturnWorking(self._name, task)) 125 | 126 | def _done(self, result): 127 | self.queue_out.put(Worker.ReturnDone(self._name, result)) 128 | 129 | def _error(self, exception, traceback): 130 | #import traceback 131 | #exception_text = traceback.format_exc() 132 | exception_text = str(exception) 133 | self.queue_out.put(Worker.ReturnError(self._name, exception_text, traceback)) 134 | 135 | def _heartbeat(self, sequence): 136 | self.queue_out.put(Worker.ReturnHeartbeat(self._name, sequence)) 137 | 138 | def __call__(self, *args, **kwargs): 139 | queue_in = self.queue_in 140 | queue_out = self.queue_out 141 | logger = logging.getLogger() 142 | logger.addHandler(QueueHandler(queue_out)) 143 | logger.setLevel(logging.DEBUG if self._debug else logging.INFO) 144 | db.init(self._settings['db_path'], False) 145 | 146 | self._ready() 147 | 148 | heartbeat_sequence = 1 149 | while True: 150 | try: 151 | task = queue_in.get(timeout=HEARTBEAT_INTERVAL) 152 | if isinstance(task, tasks.Task): 153 | self._work(str(task)) 154 | self._done(task(**self._settings)) 155 | except queues.Empty: 156 | self._heartbeat(heartbeat_sequence) 157 | heartbeat_sequence += 1 158 | except Exception as e: 159 | self._error(e, traceback.format_exc()) 160 | except KeyboardInterrupt: 161 | break 162 | 163 | def start(self): 164 | if self.is_pending(): 165 | self._status = Worker.State.RUNNING 166 | self._process = Process(target=self) 167 | self._process.start() 168 | logging.info('{0}启动'.format(self.name)) 169 | else: 170 | raise Worker.RuntimeError('Can not start worker. The worker is ' + ( 171 | 'running' if self._status == Worker.State.RUNNING else 'terminated')) 172 | 173 | def stop(self): 174 | if self.is_running(): 175 | self._status = Worker.State.TERMINATED 176 | self._process.terminate() 177 | logging.info('{0}停止'.format(self.name)) 178 | else: 179 | raise Worker.RuntimeError('Can not stop worker. The worker is ' + ( 180 | 'pending' if self._status == Worker.State.PENDING else 'terminated')) 181 | 182 | def reset(self): 183 | if self.is_terminated(): 184 | del self._process 185 | self._status = Worker.State.PENDING 186 | else: 187 | raise Worker.RuntimeError('Can not reset worker. The worker is ' + ( 188 | 'pending' if self._status == Worker.State.PENDING else 'running')) 189 | 190 | @property 191 | def status(self): 192 | if self._status == Worker.State.RUNNING: 193 | if not self._process.is_alive(): 194 | self._status = Worker.State.TERMINATED 195 | return self._status 196 | 197 | @property 198 | def status_text(self): 199 | status = self.status 200 | if status == Worker.State.PENDING: 201 | return '等待' 202 | elif status == Worker.State.RUNNING: 203 | if self.is_suspended(): 204 | return '挂起' 205 | return '运行' 206 | elif status == Worker.State.TERMINATED: 207 | return '停止' 208 | else: 209 | return '未知' 210 | 211 | def is_running(self): 212 | return self.status == Worker.State.RUNNING 213 | 214 | def is_pending(self): 215 | return self.status == Worker.State.PENDING 216 | 217 | def is_terminated(self): 218 | return self.status == Worker.State.TERMINATED 219 | 220 | def toggle_task(self, task=None): 221 | self._current_task = task 222 | 223 | def is_suspended(self): 224 | return self._current_task is None 225 | 226 | @property 227 | def current_task(self): 228 | """ 229 | 当前工作进程正在执行的任务 230 | """ 231 | return self._current_task 232 | --------------------------------------------------------------------------------