├── .gitignore ├── README.md ├── doubanhis ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── get_subject.py ├── history ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180204_1619.py │ ├── 0003_subject_poster.py │ ├── 0004_subject_daily_rank.py │ └── __init__.py ├── models.py ├── static │ └── doubanhis │ │ ├── css │ │ └── index.css │ │ ├── img │ │ └── poster │ │ │ └── 1308838.webp │ │ └── js │ │ ├── load_charts.js │ │ └── spin.js ├── tests.py ├── urls.py └── views.py ├── manage.py ├── requirements.txt └── templates ├── base.html └── history ├── about.html ├── index.html └── search.html /.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | doubanhis/__pycache__/__init__.cpython-36.pyc 3 | api_test.py 4 | db_test.py 5 | doubanhis/__pycache__/settings.cpython-36.pyc 6 | doubanhis/__pycache__/urls.cpython-36.pyc 7 | doubanhis/__pycache__/wsgi.cpython-36.pyc 8 | history/__pycache__/__init__.cpython-36.pyc 9 | history/__pycache__/admin.cpython-36.pyc 10 | history/__pycache__/models.cpython-36.pyc 11 | history/__pycache__/urls.cpython-36.pyc 12 | history/__pycache__/views.cpython-36.pyc 13 | history/migrations/__pycache__/0001_initial.cpython-36.pyc 14 | history/migrations/__pycache__/0002_auto_20180204_1619.cpython-36.pyc 15 | history/migrations/__pycache__/0003_subject_poster.cpython-36.pyc 16 | history/migrations/__pycache__/0004_subject_daily_rank.cpython-36.pyc 17 | history/migrations/__pycache__/__init__.cpython-36.pyc 18 | history/static/doubanhis/js/echarts_test.js 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DoubanHistory 2 | [![ywvT9.md.png](https://s1.ax2x.com/2018/02/17/ywvT9.md.png)](https://simimg.com/i/ywvT9) 3 | 4 | ## UPDATE 5 | * 适配移动端 6 | 7 | ![](https://ws1.sinaimg.cn/mw690/a718aca9ly1fp3850kdhkj218g280jzz.jpg) 8 | 9 | * 搜索 10 | 11 | 12 | ## TODO 13 | * 自定义标记 14 | 15 | -------------------------------------------------------------------------------- /doubanhis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SungYK/DoubanHistory/4a530b8133dd1a5a11364510d5c9b42be911114e/doubanhis/__init__.py -------------------------------------------------------------------------------- /doubanhis/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for doubanhis project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'l^h-y7jfc_4^+myv2w9yl*8#jp1*ybm16&%(of&xn%delg%t23' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = False 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'history', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'doubanhis.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [os.path.join(BASE_DIR), 'templates'], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'doubanhis.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'Asia/Shanghai' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') -------------------------------------------------------------------------------- /doubanhis/urls.py: -------------------------------------------------------------------------------- 1 | """doubanhis URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('', include('history.urls')) 22 | ] 23 | -------------------------------------------------------------------------------- /doubanhis/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for doubanhis project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "doubanhis.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /get_subject.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import sqlite3 4 | import re 5 | import sys 6 | import collections 7 | import pytz 8 | import datetime 9 | from datetime import date 10 | 11 | conn = sqlite3.connect('db.sqlite3') # datebase address 12 | cur = conn.cursor() 13 | 14 | # get data from API 15 | r = requests.get('http://api.douban.com/v2/movie/in_theaters?start=0&count=100') 16 | json_str = r.text 17 | data = json.loads(json_str) 18 | 19 | #get subject_id thus already in datebase 20 | cur.execute("select * from history_subject") 21 | saved_data = [] 22 | ids = [] 23 | for row in cur: 24 | ids.append(row[0]) 25 | 26 | new_subject = [] 27 | tz = pytz.timezone('Asia/Shanghai') 28 | today = datetime.datetime.now(tz).date().isoformat() 29 | strip_jpg = re.compile("\.jpg") 30 | 31 | # daily rank according to sort of data from douban API 32 | rank = 0 33 | for i in data['subjects']: 34 | history_rating = dict() 35 | title = i['title'] 36 | original_title = i['original_title'] 37 | sub_id = i['id'] 38 | rating = i['rating']['average'] 39 | image = i['images']['large'] 40 | image = re.sub(strip_jpg, ".webp", image) 41 | _id = sub_id 42 | 43 | # update old subject 44 | if int(i['id']) in ids: 45 | cur.execute("select update_date, history_rating from history_subject where id=?", [(int(i['id']))]) 46 | check_data = cur.fetchall() 47 | update_date = check_data[0][0] 48 | history_rating = check_data[0][1] 49 | if today != update_date: 50 | history_rating = json.loads(history_rating) 51 | history_rating[today] = rating 52 | print(history_rating) 53 | temp = sorted(history_rating.items(), key=lambda d:d[0], reverse=False) 54 | new_rating = collections.OrderedDict() 55 | for i in temp: 56 | new_rating[i[0]] = i[1] 57 | print(new_rating) 58 | history_rating = json.dumps(new_rating) 59 | update_subject = (today, history_rating, rank,int(sub_id)) 60 | cur.execute("update history_subject set update_date=?, history_rating=?, daily_rank=?, playing = 1 where id=?", update_subject) 61 | print('done') 62 | 63 | # new subject 64 | else: 65 | playing = 1 66 | pub_date = today 67 | created_date = today 68 | update_date = created_date 69 | history_rating[pub_date] = rating 70 | history_rating = json.dumps(history_rating) 71 | subject = (_id, title, original_title, sub_id, rating, history_rating, playing, pub_date, created_date, update_date, image, rank) 72 | # print(subject) 73 | new_subject.append(subject) 74 | rank += 1 75 | 76 | # commit new subject 77 | cur.executemany('insert into history_subject values(?,?,?,?,?,?,?,?,?,?,?,?)', new_subject) 78 | conn.commit() 79 | 80 | # set stop playing film 81 | cur.execute("update history_subject set playing = 0, daily_rank = -1 where update_date != ?",(today,)) 82 | conn.commit() 83 | 84 | # download poster 85 | cur.execute("select id, poster from history_subject") 86 | for row in cur: 87 | id = row[0] 88 | try: 89 | temp = re.sub(strip_webp, ".jpg", row[1]) 90 | pic = requests.get(temp) 91 | except: 92 | print('error') 93 | continue 94 | path = '/home/syk/sites/doubanhistory/DoubanHistory/history/static/doubanhis/img/poster/'+str(id)+'.jpg' 95 | fp = open(path, 'wb') 96 | fp.write(pic.content) 97 | fp.close() 98 | 99 | conn.close() 100 | -------------------------------------------------------------------------------- /history/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SungYK/DoubanHistory/4a530b8133dd1a5a11364510d5c9b42be911114e/history/__init__.py -------------------------------------------------------------------------------- /history/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /history/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HistoryConfig(AppConfig): 5 | name = 'history' 6 | -------------------------------------------------------------------------------- /history/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-04 07:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Subject', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=70)), 19 | ('original_title', models.CharField(max_length=70)), 20 | ('sub_id', models.CharField(max_length=20)), 21 | ('rating', models.DecimalField(decimal_places=1, max_digits=3)), 22 | ('history_rating', models.CharField(max_length=300)), 23 | ('playing', models.IntegerField()), 24 | ('pub_date', models.DateTimeField()), 25 | ('created_time', models.DateTimeField()), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /history/migrations/0002_auto_20180204_1619.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-04 08:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('history', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='subject', 15 | old_name='created_time', 16 | new_name='created_date', 17 | ), 18 | migrations.AddField( 19 | model_name='subject', 20 | name='update_date', 21 | field=models.DateTimeField(default='2018-02-04'), 22 | preserve_default=False, 23 | ), 24 | migrations.AlterField( 25 | model_name='subject', 26 | name='history_rating', 27 | field=models.CharField(max_length=1000), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /history/migrations/0003_subject_poster.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-04 11:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('history', '0002_auto_20180204_1619'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='subject', 15 | name='poster', 16 | field=models.CharField(default=0, max_length=200), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /history/migrations/0004_subject_daily_rank.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-04 12:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('history', '0003_subject_poster'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='subject', 15 | name='daily_rank', 16 | field=models.IntegerField(default=0), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /history/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SungYK/DoubanHistory/4a530b8133dd1a5a11364510d5c9b42be911114e/history/migrations/__init__.py -------------------------------------------------------------------------------- /history/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Subject(models.Model): 4 | title = models.CharField(max_length=70) 5 | original_title = models.CharField(max_length=70) 6 | sub_id = models.CharField(max_length=20) 7 | rating = models.DecimalField(max_digits=3, decimal_places=1) 8 | history_rating = models.CharField(max_length=1000) 9 | playing = models.IntegerField() 10 | pub_date = models.DateTimeField() 11 | created_date = models.DateTimeField() 12 | update_date = models.DateTimeField() 13 | daily_rank = models.IntegerField() 14 | poster = models.CharField(max_length=200) -------------------------------------------------------------------------------- /history/static/doubanhis/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | background-color: #E2DFDF; 4 | 5 | } 6 | 7 | .navcustom { 8 | margin:0 auto; 9 | width: 1000px; 10 | } 11 | 12 | 13 | 14 | .card { 15 | position: relative; 16 | 17 | background-color: rgb(255, 245, 240); 18 | /* background-color: #ffffff; */ 19 | /* margin: 30px 0px; */ 20 | margin:30px auto; 21 | padding: 20px 15px; 22 | text-align: center; 23 | width: 1000px; 24 | height: 350px; 25 | border-radius: 10px; 26 | box-shadow: 2px 2px 5px #9C7878; 27 | transition: 0.3s; 28 | 29 | /* border: 5px; 30 | border-style: solid */ 31 | } 32 | 33 | .card:hover { 34 | box-shadow: 2px 2px 13px #9C7878; 35 | } 36 | 37 | .poster { 38 | margin: 0 5% 0 3%; 39 | width: 225px; 40 | height: 100%; 41 | border: 5px; 42 | border-style: solid 43 | } 44 | 45 | .chart { 46 | padding-left: 5%; 47 | width: 650px; 48 | height: 300px; 49 | /* border: 3px; */ 50 | border-left: 3px dotted #eaeaea; 51 | /* border-style: solid */ 52 | } 53 | 54 | .div-inline{ 55 | float: left 56 | } 57 | 58 | .poster img{ 59 | width: 100%; 60 | height: 100% 61 | } 62 | 63 | .note { 64 | margin:30px auto; 65 | width: 1000px; 66 | text-align: center; 67 | } 68 | 69 | .hole-1{ 70 | position: absolute; 71 | width: 20px; 72 | height: 20px; 73 | border-radius: 20px; 74 | background-color: #E2DFDF; 75 | /* left: 297px; */ 76 | left: 309px; 77 | top:-10px; 78 | /* box-shadow: 0px -1px 10px #9C7878 inset; */ 79 | /* border: 3px; 80 | border-style: solid */ 81 | } 82 | .hole-2{ 83 | position: absolute; 84 | width: 20px; 85 | height: 20px; 86 | border-radius: 20px; 87 | background-color: #E2DFDF; 88 | left: 309px; 89 | bottom: -10px; 90 | box-shadow: 0px 1px 0.5px #9C7878 inset; 91 | } 92 | 93 | 94 | .load_animation{ 95 | width: 1000px; 96 | margin-top: 0px; 97 | margin-bottom: 70px; 98 | position: absolute; 99 | } 100 | 101 | .about{ 102 | position: relative; 103 | 104 | background-color: rgb(255, 245, 240); 105 | /* background-color: #ffffff; */ 106 | /* margin: 30px 0px; */ 107 | margin:30px auto; 108 | padding: 20px 50px; 109 | 110 | width: 1000px; 111 | height: 100%; 112 | 113 | box-shadow: 2px 2px 5px #9C7878; 114 | transition: 0.3s; 115 | 116 | /* border: 5px; 117 | border-style: solid */ 118 | } 119 | 120 | .about p{ 121 | font-size: 17px; 122 | margin-bottom: 20px 123 | } 124 | 125 | .about li{ 126 | font-size: 17px; 127 | font-style: oblique 128 | } 129 | 130 | .comment { 131 | position: relative; 132 | 133 | 134 | margin:30px auto; 135 | padding: 20px 50px; 136 | 137 | width: 1000px; 138 | 139 | 140 | 141 | 142 | /* border: 5px; 143 | border-style: solid */ 144 | 145 | } 146 | 147 | 148 | @media screen and (max-width: 500px) { 149 | .card { 150 | width: 100%; 151 | padding: 20px 0; 152 | /* background-color:rgb(8, 29, 36); */ 153 | } 154 | .chart { 155 | padding-left: 0; 156 | width: 100%; 157 | } 158 | .poster { 159 | width: 0%; 160 | height: 0%; 161 | border: 0; 162 | } 163 | .hole-1 { 164 | width: 0%; 165 | height: 0% 166 | } 167 | .hole-2 { 168 | width: 0%; 169 | height: 0% 170 | } 171 | .load_animation{ 172 | display: none 173 | 174 | } 175 | .note { 176 | width: 100%; 177 | } 178 | .about{ 179 | width: 100%; 180 | height: 100% 181 | } 182 | .comment { 183 | width: 100%; 184 | } 185 | .navcustom { 186 | width: 100%; 187 | } 188 | 189 | } 190 | 191 | 192 | .test{ 193 | border: 3px; 194 | border-style: solid 195 | } 196 | 197 | footer { 198 | position: relative; 199 | bottom:0; 200 | width:100%; 201 | height:50px; 202 | text-align: center; 203 | 204 | } -------------------------------------------------------------------------------- /history/static/doubanhis/img/poster/1308838.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SungYK/DoubanHistory/4a530b8133dd1a5a11364510d5c9b42be911114e/history/static/doubanhis/img/poster/1308838.webp -------------------------------------------------------------------------------- /history/static/doubanhis/js/load_charts.js: -------------------------------------------------------------------------------- 1 | 2 | function initChart(element, title, rating, id) { 3 | var myChart = echarts.init(element); 4 | var now = new Date() 5 | var data = [] 6 | var dataX = [] 7 | var dataY = [] 8 | var today = 0 9 | 10 | empty_data_flag = 1 11 | 12 | // gap 13 | dataX.push(" ") 14 | dataY.push(null) 15 | 16 | // first day tag 17 | firstday = 1 18 | for(var key in rating) { 19 | var year = key.substring(0,4) 20 | var month = key.substring(5, key.length) 21 | if(rating[key] != 0) { 22 | if(firstday==1) { 23 | pub_day = rating[key] 24 | firstday = 0 25 | } 26 | dataX.push(year+"\n"+month) 27 | dataY.push(rating[key]) 28 | today = rating[key] 29 | empty_data_flag = 0 30 | } 31 | 32 | // data.push(getData(new Date(key), rating[key])) 33 | 34 | } 35 | // gap 36 | dataX.push(" ") 37 | dataY.push(null) 38 | // console.log(dataX) 39 | 40 | if (empty_data_flag == 1) { 41 | title = title + " (暂无数据)" 42 | pub_day = today 43 | } 44 | 45 | // 指定图表的配置项和数据 46 | var option = { 47 | title: { 48 | text: title, 49 | padding: [ 50 | 5, // 上 51 | 5, // 右 52 | 5, // 下 53 | 30, // 左 54 | ], 55 | subtext : "豆瓣链接", 56 | sublink: 'https://movie.douban.com/subject/'+id 57 | }, 58 | tooltip: { 59 | trigger: 'axis' 60 | }, 61 | grid: { 62 | show: false 63 | }, 64 | xAxis: { 65 | type: 'category', 66 | splitLine: { 67 | show: false 68 | }, 69 | boundaryGap:false, 70 | data: dataX 71 | }, 72 | yAxis: { 73 | min: function (value) { 74 | if(value.min - 0.5 <= 0) 75 | return 0 76 | return value.min - 0.5; 77 | }, 78 | max: function (value) { 79 | return value.max + 0.5; 80 | }, 81 | // min:0, 82 | // max:10, 83 | // silent: true, 84 | 85 | type: 'value' 86 | }, 87 | dataZoom: [ 88 | { 89 | type: 'inside', 90 | start: 0, 91 | end: 100 92 | } 93 | ], 94 | series: [{ 95 | data: dataY, 96 | type: 'line', 97 | markPoint: { 98 | data: [ 99 | {coord: [dataY.length-2, today], name: '今日评分', value: today}, 100 | {coord: [1, pub_day], name: '首日评分', value: pub_day} 101 | ] 102 | }, 103 | }], 104 | }; 105 | // myChart.showLoading() 106 | // 使用刚指定的配置项和数据显示图表。 107 | 108 | myChart.setOption(option) 109 | 110 | 111 | } 112 | 113 | function getData(date, rate) { 114 | 115 | return { 116 | name: date.toString(), 117 | value: [ 118 | [date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-'), 119 | rate 120 | ] 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /history/static/doubanhis/js/spin.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || Object.assign || function(t) { 2 | for (var s, i = 1, n = arguments.length; i < n; i++) { 3 | s = arguments[i]; 4 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 5 | t[p] = s[p]; 6 | } 7 | return t; 8 | }; 9 | var defaults = { 10 | lines: 12, 11 | length: 7, 12 | width: 5, 13 | radius: 10, 14 | scale: 1.0, 15 | corners: 1, 16 | color: '#000', 17 | fadeColor: 'transparent', 18 | opacity: 0.25, 19 | rotate: 0, 20 | direction: 1, 21 | speed: 1, 22 | trail: 100, 23 | fps: 20, 24 | zIndex: 2e9, 25 | className: 'spinner', 26 | top: '50%', 27 | left: '50%', 28 | shadow: 'none', 29 | position: 'absolute', 30 | }; 31 | var Spinner = /** @class */ (function () { 32 | function Spinner(opts) { 33 | if (opts === void 0) { opts = {}; } 34 | this.opts = __assign({}, defaults, opts); 35 | } 36 | /** 37 | * Adds the spinner to the given target element. If this instance is already 38 | * spinning, it is automatically removed from its previous target by calling 39 | * stop() internally. 40 | */ 41 | Spinner.prototype.spin = function (target) { 42 | var _this = this; 43 | this.stop(); 44 | this.el = document.createElement('div'); 45 | this.el.className = this.opts.className; 46 | this.el.setAttribute('role', 'progressbar'); 47 | css(this.el, { 48 | position: this.opts.position, 49 | width: 0, 50 | zIndex: this.opts.zIndex, 51 | left: this.opts.left, 52 | top: this.opts.top, 53 | transform: "scale(" + this.opts.scale + ")", 54 | }); 55 | if (target) { 56 | target.insertBefore(this.el, target.firstChild || null); 57 | } 58 | var animator; 59 | var getNow; 60 | if (typeof requestAnimationFrame !== 'undefined') { 61 | animator = requestAnimationFrame; 62 | getNow = function () { return performance.now(); }; 63 | } 64 | else { 65 | // fallback for IE 9 66 | animator = function (callback) { return setTimeout(callback, 1000 / _this.opts.fps); }; 67 | getNow = function () { return Date.now(); }; 68 | } 69 | var lastFrameTime; 70 | var state = 0; // state is rotation percentage (between 0 and 1) 71 | var animate = function () { 72 | var time = getNow(); 73 | if (lastFrameTime === undefined) { 74 | lastFrameTime = time - 1; 75 | } 76 | state += getAdvancePercentage(time - lastFrameTime, _this.opts.speed); 77 | lastFrameTime = time; 78 | if (state > 1) { 79 | state -= Math.floor(state); 80 | } 81 | if (_this.el.childNodes.length === _this.opts.lines) { 82 | for (var line = 0; line < _this.opts.lines; line++) { 83 | var opacity = getLineOpacity(line, state, _this.opts); 84 | _this.el.childNodes[line].childNodes[0].style.opacity = opacity.toString(); 85 | } 86 | } 87 | _this.animateId = _this.el ? animator(animate) : undefined; 88 | }; 89 | drawLines(this.el, this.opts); 90 | animate(); 91 | return this; 92 | }; 93 | /** 94 | * Stops and removes the Spinner. 95 | * Stopped spinners may be reused by calling spin() again. 96 | */ 97 | Spinner.prototype.stop = function () { 98 | if (this.el) { 99 | if (typeof requestAnimationFrame !== 'undefined') { 100 | cancelAnimationFrame(this.animateId); 101 | } 102 | else { 103 | clearTimeout(this.animateId); 104 | } 105 | if (this.el.parentNode) { 106 | this.el.parentNode.removeChild(this.el); 107 | } 108 | this.el = undefined; 109 | } 110 | return this; 111 | }; 112 | return Spinner; 113 | }()); 114 | 115 | function getAdvancePercentage(msSinceLastFrame, roundsPerSecond) { 116 | return msSinceLastFrame / 1000 * roundsPerSecond; 117 | } 118 | function getLineOpacity(line, state, opts) { 119 | var linePercent = (line + 1) / opts.lines; 120 | var diff = state - (linePercent * opts.direction); 121 | if (diff < 0 || diff > 1) { 122 | diff += opts.direction; 123 | } 124 | // opacity should start at 1, and approach opacity option as diff reaches trail percentage 125 | var trailPercent = opts.trail / 100; 126 | var opacityPercent = 1 - diff / trailPercent; 127 | if (opacityPercent < 0) { 128 | return opts.opacity; 129 | } 130 | var opacityDiff = 1 - opts.opacity; 131 | return opacityPercent * opacityDiff + opts.opacity; 132 | } 133 | /** 134 | * Tries various vendor prefixes and returns the first supported property. 135 | */ 136 | function vendor(el, prop) { 137 | if (el.style[prop] !== undefined) { 138 | return prop; 139 | } 140 | // needed for transform properties in IE 9 141 | var prefixed = 'ms' + prop.charAt(0).toUpperCase() + prop.slice(1); 142 | if (el.style[prefixed] !== undefined) { 143 | return prefixed; 144 | } 145 | return ''; 146 | } 147 | /** 148 | * Sets multiple style properties at once. 149 | */ 150 | function css(el, props) { 151 | for (var prop in props) { 152 | el.style[vendor(el, prop) || prop] = props[prop]; 153 | } 154 | return el; 155 | } 156 | /** 157 | * Returns the line color from the given string or array. 158 | */ 159 | function getColor(color, idx) { 160 | return typeof color == 'string' ? color : color[idx % color.length]; 161 | } 162 | /** 163 | * Internal method that draws the individual lines. 164 | */ 165 | function drawLines(el, opts) { 166 | var borderRadius = (Math.round(opts.corners * opts.width * 500) / 1000) + 'px'; 167 | var shadow = 'none'; 168 | if (opts.shadow === true) { 169 | shadow = '0 2px 4px #000'; // default shadow 170 | } 171 | else if (typeof opts.shadow === 'string') { 172 | shadow = opts.shadow; 173 | } 174 | var shadows = parseBoxShadow(shadow); 175 | for (var i = 0; i < opts.lines; i++) { 176 | var degrees = ~~(360 / opts.lines * i + opts.rotate); 177 | var backgroundLine = css(document.createElement('div'), { 178 | position: 'absolute', 179 | top: -opts.width / 2 + "px", 180 | width: (opts.length + opts.width) + 'px', 181 | height: opts.width + 'px', 182 | background: getColor(opts.fadeColor, i), 183 | borderRadius: borderRadius, 184 | transformOrigin: 'left', 185 | transform: "rotate(" + degrees + "deg) translateX(" + opts.radius + "px)", 186 | }); 187 | var line = css(document.createElement('div'), { 188 | width: '100%', 189 | height: '100%', 190 | background: getColor(opts.color, i), 191 | borderRadius: borderRadius, 192 | boxShadow: normalizeShadow(shadows, degrees), 193 | opacity: opts.opacity, 194 | }); 195 | backgroundLine.appendChild(line); 196 | el.appendChild(backgroundLine); 197 | } 198 | } 199 | function parseBoxShadow(boxShadow) { 200 | var regex = /^\s*([a-zA-Z]+\s+)?(-?\d+(\.\d+)?)([a-zA-Z]*)\s+(-?\d+(\.\d+)?)([a-zA-Z]*)(.*)$/; 201 | var shadows = []; 202 | for (var _i = 0, _a = boxShadow.split(','); _i < _a.length; _i++) { 203 | var shadow = _a[_i]; 204 | var matches = shadow.match(regex); 205 | if (matches === null) { 206 | continue; // invalid syntax 207 | } 208 | var x = +matches[2]; 209 | var y = +matches[5]; 210 | var xUnits = matches[4]; 211 | var yUnits = matches[7]; 212 | if (x === 0 && !xUnits) { 213 | xUnits = yUnits; 214 | } 215 | if (y === 0 && !yUnits) { 216 | yUnits = xUnits; 217 | } 218 | if (xUnits !== yUnits) { 219 | continue; // units must match to use as coordinates 220 | } 221 | shadows.push({ 222 | prefix: matches[1] || '', 223 | x: x, 224 | y: y, 225 | xUnits: xUnits, 226 | yUnits: yUnits, 227 | end: matches[8], 228 | }); 229 | } 230 | return shadows; 231 | } 232 | /** 233 | * Modify box-shadow x/y offsets to counteract rotation 234 | */ 235 | function normalizeShadow(shadows, degrees) { 236 | var normalized = []; 237 | for (var _i = 0, shadows_1 = shadows; _i < shadows_1.length; _i++) { 238 | var shadow = shadows_1[_i]; 239 | var xy = convertOffset(shadow.x, shadow.y, degrees); 240 | normalized.push(shadow.prefix + xy[0] + shadow.xUnits + ' ' + xy[1] + shadow.yUnits + shadow.end); 241 | } 242 | return normalized.join(', '); 243 | } 244 | function convertOffset(x, y, degrees) { 245 | var radians = degrees * Math.PI / 180; 246 | var sin = Math.sin(radians); 247 | var cos = Math.cos(radians); 248 | return [ 249 | Math.round((x * cos + y * sin) * 1000) / 1000, 250 | Math.round((-x * sin + y * cos) * 1000) / 1000, 251 | ]; 252 | } -------------------------------------------------------------------------------- /history/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /history/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from . import views 3 | 4 | urlpatterns = [ 5 | url(r'^$', views.index, name='index'), 6 | url(r'^about$', views.about, name='about'), 7 | url(r'^search/$', views.search, name='search') 8 | ] -------------------------------------------------------------------------------- /history/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse 3 | from .models import Subject 4 | import json 5 | 6 | def index(request): 7 | subject_list = Subject.objects.filter(playing=1).order_by('daily_rank') 8 | 9 | lst = [] 10 | for i in subject_list: 11 | lst.append([i.pk, i.title, i.original_title, i.history_rating]) 12 | 13 | return render(request, 'history/index.html', context={'lst':lst}) 14 | 15 | def about(request): 16 | return render(request, 'history/about.html') 17 | 18 | def search(request): 19 | q = request.GET.get('q') 20 | if q == None or q == '': 21 | return render(request, 'history/search.html', context={'query':q, 'lst':[]}) 22 | 23 | subject_list = Subject.objects.filter(title__icontains=q) 24 | 25 | lst = [] 26 | for i in subject_list: 27 | lst.append([i.pk, i.title, i.original_title, i.history_rating]) 28 | 29 | return render(request, 'history/search.html', context={'query':q, 'lst':lst}) -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "doubanhis.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.0.2 2 | pytz==2017.3 3 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 32 | 33 | 34 | 豆瓣历史30天 35 | 36 | 37 | 38 | 72 | 73 | {% block main %} {% endblock main %} 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /templates/history/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} {% block main %} 2 |
3 |
4 | 5 |

ABOUT

6 |
7 | 8 |
9 | 10 |

# INTRO

11 | 12 |
13 |
14 |

这是一个收集院线上映电影的豆瓣历史评分的网站,源于突然有一天想了解一部电影豆瓣评分变化,搜索之后发现还没有这样的网站, 也确实有人手动截图在记录关心的影片/剧集的评分变化,特别是在讨论关于水军/刷分等话题时,于是就自己写了一个。 15 | 由于是第一次做,没什么经验,目前还是比较简陋,更多东西会慢慢完善。 16 | 17 |

18 |

19 | 如果对你有所帮助,希望你可以分享给其他人。 20 |

21 | 37 | 38 | 39 |

# CONTACT

40 | 41 |
42 |
43 |

E-mail: cirillaye#outlook.com

44 |

微博: 45 | @Guling_Street 46 |

47 |

48 | Github 49 |

50 |

51 | ↓↓↓ 反馈 ↓↓↓ (加载可能有些慢) 52 |

53 | 54 |
55 | 56 |
57 | 58 |
59 | 72 | 73 |
74 | 75 |
76 | 77 |
78 | 79 | {% endblock main %} -------------------------------------------------------------------------------- /templates/history/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block main %} 4 |
5 |
6 | 7 | 8 |

9 | * 数据来源于豆瓣 API 10 |

11 |

12 | * 收录正在上映影片 13 |

14 |
15 | 16 |
17 | 18 |
19 | 20 | 28 | 29 |
30 | 31 |
32 |
33 | 34 | 35 | 36 | 99 | 100 | 108 | 109 |
110 | 111 | {% endblock main %} -------------------------------------------------------------------------------- /templates/history/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block main %} 3 | 4 | 5 |
6 | 7 |
8 |
9 |
10 | 45 | {% endblock main %} --------------------------------------------------------------------------------