├── .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 |
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 |
9 |
19 |
24 |
25 |
26 |
27 |
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 |
9 |
19 |
24 |
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 |
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 |
10 |
20 |
25 |
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 |
8 |
9 |
10 | 工具
11 |
12 |
13 | 控制台
14 |
15 |
16 |
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 | {{ worker.name }}
37 | {{ worker.status_text }}
38 | {{ worker.current_task }}
39 |
40 | {% end %}
41 |
42 |
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 | {{ task.name }}
65 |
66 | {% end %}
67 |
68 |
69 | {% end %}
70 |
71 | 新建
72 | 刷新
73 |
74 |
75 |
76 |
82 |
83 |
84 |
85 |
86 |
87 |
91 |
92 |
93 |
94 |
选择帐号(按 Ctrl 或 Shift 多选):
95 |
96 |
97 | {% for row in accounts %}
98 | {% if row.user %}
99 | {{ row.user.name }}({{ row.name }})
100 | {% else %}
101 | {{ row.name }}
102 | {% end %}
103 | {% end %}
104 |
105 |
106 |
107 |
108 |
选择任务类型(按 Ctrl 或 Shift 多选):
109 |
110 |
111 | {% for task in all_tasks %}
112 | {{ task }}
113 | {% end %}
114 |
115 |
116 |
117 |
118 |
119 |
123 |
124 |
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 |
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 |
14 |
15 |
16 | 帮助
17 |
18 |
19 | 用户手册
20 |
21 |
22 |
23 |
24 |
25 |
26 | {% end %}
27 |
28 | {% block body_extra %}
29 |
41 | {% end %}
42 |
--------------------------------------------------------------------------------
/src/service/views/modules/account.html:
--------------------------------------------------------------------------------
1 |
2 | {% if account is None %}
3 |
登录
4 | {% else %}
5 |
24 | {% end %}
25 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
10 |
20 |
25 |
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 |
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 |
9 |
19 |
24 |
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 |
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 |
16 |
17 |
18 |
我拉黑 {{ total_rows }} 人
19 |
20 |
21 |
22 |
23 | 备份
24 |
25 |
26 | 查看历史记录
27 |
28 |
29 |
30 | {% end %}
31 |
32 | {% try %}{% if search %}
33 |
34 |
35 |
搜索“{{ search }}”的结果
36 |
37 |
42 |
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 |
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 |
7 |
8 |
9 |
我曾经拉黑 {{ total_rows }} 人
10 |
11 |
12 |
17 |
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 |
17 |
18 |
19 |
{{ '读过' if status == 'done' else '想读' if status == 'wish' else '在读' }} {{ total_rows }} 本
20 |
21 |
22 |
23 |
24 | 备份
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% end %}
32 |
33 | {% try %}{% if search %}
34 |
35 |
36 |
搜索“{{ search }}”的结果
37 |
38 |
43 |
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 |
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 |
9 |
10 |
11 |
我曾标记过 {{ total_rows }} 本书
12 |
13 |
14 |
19 |
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 |
19 |
20 |
21 |
发布 {{ total_rows }} 条
22 |
23 |
24 |
29 |
30 | {% end %}
31 |
32 | {% try %}{% if search %}
33 |
34 |
35 |
搜索“{{ search }}”的结果
36 |
37 |
42 |
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 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
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 |
23 | {% block headline %}
24 |
25 |
26 |
27 |
喜欢的日记 {{ total_rows }} 篇
28 |
29 |
30 |
35 |
36 | {% end %}
37 |
38 | {% try %}{% if search %}
39 |
40 |
41 |
搜索“{{ search }}”的结果
42 |
43 |
48 |
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 |
20 | {% block headline %}
21 |
22 |
23 |
24 |
喜欢的相册 {{ total_rows }} 本
25 |
26 |
27 |
32 |
33 | {% end %}
34 |
35 | {% try %}{% if search %}
36 |
37 |
38 |
搜索“{{ search }}”的结果
39 |
40 |
45 |
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 |
16 |
17 |
18 |
我被 {{ total_rows }} 人关注
19 |
20 |
21 |
22 |
23 | 备份
24 |
25 |
26 | 查看历史记录
27 |
28 |
29 |
30 | {% end %}
31 |
32 | {% try %}{% if search %}
33 |
34 |
35 |
搜索“{{ search }}”的结果
36 |
37 |
42 |
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 |
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 |
7 |
8 |
9 |
我曾经被 {{ total_rows }} 人关注
10 |
11 |
12 |
17 |
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 |
16 |
17 |
18 |
我关注 {{ total_rows }} 人
19 |
20 |
21 |
22 |
23 | 备份
24 |
25 |
26 | 查看历史记录
27 |
28 |
29 |
30 | {% end %}
31 |
32 | {% try %}{% if search %}
33 |
34 |
35 |
搜索“{{ search }}”的结果
36 |
37 |
42 |
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 |
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 |
7 |
8 |
9 |
我曾经关注 {{ total_rows }} 人
10 |
11 |
12 |
17 |
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 |
17 |
18 |
19 |
{{ '看过' if status == 'done' else '想看' if status == 'wish' else '在看' }} {{ total_rows }} 部
20 |
21 |
22 |
23 |
24 | 备份
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% end %}
32 |
33 | {% try %}{% if search %}
34 |
35 |
36 |
搜索“{{ search }}”的结果
37 |
38 |
43 |
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 |
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 |
17 |
18 |
19 |
{{ '听过' if status == 'done' else '想听' if status == 'wish' else '在听' }} {{ total_rows }} 张
20 |
21 |
22 |
23 |
24 | 备份
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% end %}
32 |
33 | {% try %}{% if search %}
34 |
35 |
36 |
搜索“{{ search }}”的结果
37 |
38 |
43 |
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 |
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 |
7 |
8 |
9 |
我曾经关注 {{ total_rows }} 人
10 |
11 |
12 |
17 |
18 | {% end %}
--------------------------------------------------------------------------------
/src/service/views/my/my.html:
--------------------------------------------------------------------------------
1 | {% extends "../themes/main.html" %}
2 |
3 | {% block search %}
4 |
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 |
17 |
18 |
19 |
写日记 {{ total_rows }} 篇
20 |
21 |
22 |
27 |
28 | {% end %}
29 |
30 | {% try %}{% if search %}
31 |
32 |
33 |
搜索“{{ search }}”的结果
34 |
35 |
40 |
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 |
16 |
17 |
18 |
相册 {{ total_rows }} 本
19 |
20 |
21 |
26 |
27 | {% end %}
28 |
29 | {% try %}{% if search %}
30 |
31 |
32 |
搜索“{{ search }}”的结果
33 |
34 |
39 |
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 |
13 |
23 |
24 |
25 |
26 | 另存为
27 |
28 |
29 |
30 |
31 |
32 |
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 |
13 |
23 |
28 |
29 |
30 |
31 |
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 |
15 |
16 |
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 |
8 |
9 |
10 | 工具
11 |
12 |
13 | 设置
14 |
15 |
16 |
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 |
40 |
41 |
42 | {% if row.user %}
43 |
{{ row.user.name }} ({{ row.name}})
44 |
{{ row.user.signature }}
45 | {% else %}
46 |
{{ row.name}}
47 | {% end %}
48 |
49 |
50 |
51 |
52 | 登录时间
53 | {{ row.created }}
54 |
55 |
56 |
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 |
17 |
18 |
19 | 工具
20 |
21 |
22 | 设置
23 |
24 |
25 | 帐号
26 |
27 |
28 | 登录
29 |
30 |
31 |
32 |
33 |
34 | 程序使用豆瓣官方网页登录,不会获取和保存你的密码。为了保障你的帐号安全,请从官方渠道下载本软件。
35 |
36 |
37 |
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 |
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 |
23 | {% end %}
--------------------------------------------------------------------------------
/src/service/views/settings/settings.html:
--------------------------------------------------------------------------------
1 | {% extends "../themes/main.html" %}
2 |
3 | {% block title %}设置{% end %}
4 |
5 | {% block main %}
6 |
7 |
8 |
9 |
10 | 工具
11 |
12 |
13 | 设置
14 |
15 |
16 |
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 |
9 |
19 |
24 |
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 |
--------------------------------------------------------------------------------