##// END OF EJS Templates
Search duplicates for file, not hash
neko259 -
r1827:bbdc98ff default
parent child Browse files
Show More
@@ -1,231 +1,231 b''
1 1 import re
2 2
3 3 from PIL import Image
4 4
5 5 from django.contrib.staticfiles import finders
6 6 from django.contrib.staticfiles.templatetags.staticfiles import static
7 7 from django.core.files.images import get_image_dimensions
8 8 from django.template.defaultfilters import filesizeformat
9 9 from django.core.urlresolvers import reverse
10 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 11
12 12 from boards.utils import get_domain, cached_result
13 13 from boards import settings
14 14
15 15
16 16 FILE_STUB_IMAGE = 'images/file.png'
17 17 FILE_STUB_URL = 'url'
18 18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19 19
20 20
21 21 FILE_TYPES_VIDEO = (
22 22 'webm',
23 23 'mp4',
24 24 'mpeg',
25 25 'ogv',
26 26 )
27 27 FILE_TYPE_SVG = 'svg'
28 28 FILE_TYPES_AUDIO = (
29 29 'ogg',
30 30 'mp3',
31 31 'opus',
32 32 )
33 33 FILE_TYPES_IMAGE = (
34 34 'jpeg',
35 35 'jpg',
36 36 'png',
37 37 'bmp',
38 38 'gif',
39 39 )
40 40
41 41 PLAIN_FILE_FORMATS = {
42 42 'zip': 'archive',
43 43 'tar': 'archive',
44 44 'gz': 'archive',
45 45 'mid' : 'midi',
46 46 }
47 47
48 48 URL_PROTOCOLS = {
49 49 'magnet': 'magnet',
50 50 }
51 51
52 52 CSS_CLASS_IMAGE = 'image'
53 53 CSS_CLASS_THUMB = 'thumb'
54 54
55 55
56 56 def get_viewers():
57 57 return AbstractViewer.__subclasses__()
58 58
59 59
60 60 def get_static_dimensions(filename):
61 61 file_path = finders.find(filename)
62 62 return get_image_dimensions(file_path)
63 63
64 64
65 65 # TODO Move this to utils
66 66 def file_exists(filename):
67 67 return finders.find(filename) is not None
68 68
69 69
70 70 class AbstractViewer:
71 71 def __init__(self, file, file_type, hash, url):
72 72 self.file = file
73 73 self.file_type = file_type
74 74 self.hash = hash
75 75 self.url = url
76 76
77 77 @staticmethod
78 78 def supports(file_type):
79 79 return True
80 80
81 81 def get_view(self):
82 82 search_host = settings.get('External', 'ImageSearchHost')
83 83 if search_host:
84 84 if search_host.endswith('/'):
85 85 search_host = search_host[:-1]
86 86 search_url = search_host + self.file.url
87 87 else:
88 88 search_url = ''
89 89
90 90 return '<div class="image">'\
91 91 '{}'\
92 92 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
93 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-hash="{}">πŸ” </a></div>'\
93 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
94 94 '</div>'.format(self.get_format_view(), self.file.url,
95 95 self.file_type, filesizeformat(self.file.size),
96 self.file_type, search_url, self.hash)
96 self.file_type, search_url, self.file.name)
97 97
98 98 def get_format_view(self):
99 99 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
100 100 file_name = FILE_FILEFORMAT.format(image_name)
101 101
102 102 if file_exists(file_name):
103 103 image = file_name
104 104 else:
105 105 image = FILE_STUB_IMAGE
106 106
107 107 w, h = get_static_dimensions(image)
108 108
109 109 return '<a href="{}">'\
110 110 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
111 111 '</a>'.format(self.file.url, static(image), w, h)
112 112
113 113
114 114 class VideoViewer(AbstractViewer):
115 115 @staticmethod
116 116 def supports(file_type):
117 117 return file_type in FILE_TYPES_VIDEO
118 118
119 119 def get_format_view(self):
120 120 return '<video width="200" height="150" controls src="{}"></video>'\
121 121 .format(self.file.url)
122 122
123 123
124 124 class AudioViewer(AbstractViewer):
125 125 @staticmethod
126 126 def supports(file_type):
127 127 return file_type in FILE_TYPES_AUDIO
128 128
129 129 def get_format_view(self):
130 130 return '<audio controls src="{}"></audio>'.format(self.file.url)
131 131
132 132
133 133 class SvgViewer(AbstractViewer):
134 134 @staticmethod
135 135 def supports(file_type):
136 136 return file_type == FILE_TYPE_SVG
137 137
138 138 def get_format_view(self):
139 139 return '<a class="thumb" href="{}">'\
140 140 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
141 141 '</a>'.format(self.file.url, self.file.url)
142 142
143 143
144 144 class ImageViewer(AbstractViewer):
145 145 @staticmethod
146 146 def supports(file_type):
147 147 return file_type in FILE_TYPES_IMAGE
148 148
149 149 def get_format_view(self):
150 150 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
151 151 filesizeformat(self.file.size))
152 152
153 153 try:
154 154 width, height = get_image_dimensions(self.file.path)
155 155 except Exception:
156 156 # If the image is a decompression bomb, treat it as just a regular
157 157 # file
158 158 return super().get_format_view()
159 159
160 160 preview_path = self.file.path.replace('.', '.200x150.')
161 161 pre_width, pre_height = get_image_dimensions(preview_path)
162 162
163 163 split = self.file.url.rsplit('.', 1)
164 164 w, h = 200, 150
165 165 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
166 166
167 167 return '<a class="{}" href="{full}">' \
168 168 '<img class="post-image-preview"' \
169 169 ' src="{}"' \
170 170 ' alt="{}"' \
171 171 ' width="{}"' \
172 172 ' height="{}"' \
173 173 ' data-width="{}"' \
174 174 ' data-height="{}" />' \
175 175 '</a>' \
176 176 .format(CSS_CLASS_THUMB,
177 177 thumb_url,
178 178 self.hash,
179 179 str(pre_width),
180 180 str(pre_height), str(width), str(height),
181 181 full=self.file.url, image_meta=metadata)
182 182
183 183
184 184 class UrlViewer(AbstractViewer):
185 185 @staticmethod
186 186 def supports(file_type):
187 187 return file_type is None
188 188
189 189 def get_view(self):
190 190 return '<div class="image">' \
191 191 '{}' \
192 192 '<div class="image-metadata">{}</div>' \
193 193 '</div>'.format(self.get_format_view(), get_domain(self.url))
194 194
195 195 def get_format_view(self):
196 196 protocol = self.url.split(':')[0]
197 197
198 198 domain = get_domain(self.url)
199 199
200 200 if protocol in URL_PROTOCOLS:
201 201 url_image_name = URL_PROTOCOLS.get(protocol)
202 202 elif domain:
203 203 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
204 204 else:
205 205 url_image_name = FILE_STUB_URL
206 206
207 207 image_path = 'images/{}.png'.format(url_image_name)
208 208 image = static(image_path)
209 209 w, h = get_static_dimensions(image_path)
210 210
211 211 return '<a href="{}">' \
212 212 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
213 213 '</a>'.format(self.url, image, w, h)
214 214
215 215 @cached_result()
216 216 def _find_image_for_domains(self, domain):
217 217 """
218 218 Searches for the domain image for every domain level except top.
219 219 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
220 220 example.co.uk, then co.uk
221 221 """
222 222 levels = domain.split('.')
223 223 while len(levels) > 1:
224 224 domain = '.'.join(levels)
225 225
226 226 filename = 'images/domains/{}.png'.format(domain)
227 227 if file_exists(filename):
228 228 return 'domains/' + domain
229 229 else:
230 230 del levels[0]
231 231
@@ -1,220 +1,220 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var ITEM_VOLUME_LEVEL = 'volumeLevel';
27 27 var IMAGE_TYPES = ['png', 'jpg', 'jpeg', 'bmp'];
28 28
29 29 /**
30 30 * An email is a hidden file to prevent spam bots from posting. It has to be
31 31 * hidden.
32 32 */
33 33 function hideEmailFromForm() {
34 34 $('.form-email').parent().parent().hide();
35 35 }
36 36
37 37 /**
38 38 * Highlight code blocks with code highlighter
39 39 */
40 40 function highlightCode(node) {
41 41 node.find('pre code').each(function(i, e) {
42 42 hljs.highlightBlock(e);
43 43 });
44 44 }
45 45
46 46 function updateFavPosts(data) {
47 47 var includePostBody = $('#fav-panel').is(":visible");
48 48
49 49 var allNewPostCount = 0;
50 50
51 51 if (includePostBody) {
52 52 var favoriteThreadPanel = $('#fav-panel');
53 53 favoriteThreadPanel.empty();
54 54 }
55 55
56 56 $.each($.parseJSON(data), function (_, dict) {
57 57 var newPostCount = dict.new_post_count;
58 58 allNewPostCount += newPostCount;
59 59
60 60 if (includePostBody) {
61 61 var favThreadNode = $('<div class="post"></div>');
62 62 favThreadNode.append($(dict.post_url));
63 63 favThreadNode.append(' ');
64 64 favThreadNode.append($('<span class="title">' + dict.title + '</span>'));
65 65
66 66 if (newPostCount > 0) {
67 67 favThreadNode.append(' (<a href="' + dict.newest_post_link + '">+' + newPostCount + "</a>)");
68 68 }
69 69
70 70 favoriteThreadPanel.append(favThreadNode);
71 71
72 72 addRefLinkPreview(favThreadNode[0]);
73 73 }
74 74 });
75 75
76 76 var newPostCountNode = $('#new-fav-post-count');
77 77 if (allNewPostCount > 0) {
78 78 newPostCountNode.text('(+' + allNewPostCount + ')');
79 79 newPostCountNode.show();
80 80 } else {
81 81 newPostCountNode.hide();
82 82 }
83 83 }
84 84
85 85 function initFavPanel() {
86 86 var favPanelButton = $('#fav-panel-btn');
87 87 if (favPanelButton.length > 0 && typeof SharedWorker != 'undefined') {
88 88 var worker = new SharedWorker($('body').attr('data-update-script'));
89 89 worker.port.onmessage = function(e) {
90 90 updateFavPosts(e.data);
91 91 };
92 92 worker.onerror = function(event){
93 93 throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
94 94 };
95 95 worker.port.start();
96 96
97 97 $(favPanelButton).click(function() {
98 98 var favPanel = $('#fav-panel');
99 99 favPanel.toggle();
100 100
101 101 worker.port.postMessage({ includePostBody: favPanel.is(':visible')});
102 102
103 103 return false;
104 104 });
105 105
106 106 $(document).on('keyup.removepic', function(e) {
107 107 if(e.which === 27) {
108 108 $('#fav-panel').hide();
109 109 }
110 110 });
111 111 }
112 112 }
113 113
114 114 function setVolumeLevel(level) {
115 115 localStorage.setItem(ITEM_VOLUME_LEVEL, level);
116 116 }
117 117
118 118 function getVolumeLevel() {
119 119 var level = localStorage.getItem(ITEM_VOLUME_LEVEL);
120 120 if (level == null) {
121 121 level = 1.0;
122 122 }
123 123 return level
124 124 }
125 125
126 126 function processVolumeUser(node) {
127 127 if (!window.localStorage) return;
128 128 node.prop("volume", getVolumeLevel());
129 129 node.on('volumechange', function(event) {
130 130 setVolumeLevel(event.target.volume);
131 131 $("video,audio").prop("volume", getVolumeLevel());
132 132 });
133 133 }
134 134
135 135 /**
136 136 * Add all scripts than need to work on post, when the post is added to the
137 137 * document.
138 138 */
139 139 function addScriptsToPost(post) {
140 140 addRefLinkPreview(post[0]);
141 141 highlightCode(post);
142 142 processVolumeUser(post.find("video,audio"));
143 143 }
144 144
145 145 /**
146 146 * Fix compatibility issues with some rare browsers
147 147 */
148 148 function compatibilityCrutches() {
149 149 if (window.operamini) {
150 150 $('#form textarea').each(function() { this.placeholder = ''; });
151 151 }
152 152 }
153 153
154 154 function addContextMenu() {
155 155 $.contextMenu({
156 156 selector: '.file-menu',
157 157 trigger: 'left',
158 158
159 159 build: function($trigger, e) {
160 160 var fileSearchUrl = $trigger.data('search-url');
161 161 var isImage = IMAGE_TYPES.indexOf($trigger.data('type')) > -1;
162 162 var hasUrl = fileSearchUrl.length > 0;
163 163 return {
164 164 items: {
165 165 duplicates: {
166 166 name: gettext('Duplicates search'),
167 167 callback: function(key, opts) {
168 window.location = '/feed/?image_hash=' + $trigger.data('hash');
168 window.location = '/feed/?image=' + $trigger.data('filename');
169 169 }
170 170 },
171 171 google: {
172 172 name: 'Google',
173 173 visible: isImage && hasUrl,
174 174 callback: function(key, opts) {
175 175 window.location = 'https://www.google.com/searchbyimage?image_url=' + fileSearchUrl;
176 176 }
177 177 },
178 178 iqdb: {
179 179 name: 'IQDB',
180 180 visible: isImage && hasUrl,
181 181 callback: function(key, opts) {
182 182 window.location = 'http://iqdb.org/?url=' + fileSearchUrl;
183 183 }
184 184 },
185 185 tineye: {
186 186 name: 'TinEye',
187 187 visible: isImage && hasUrl,
188 188 callback: function(key, opts) {
189 189 window.location = 'http://tineye.com/search?url=' + fileSearchUrl;
190 190 }
191 191 }
192 192 },
193 193 };
194 194 }
195 195 });
196 196 }
197 197
198 198 $( document ).ready(function() {
199 199 hideEmailFromForm();
200 200
201 201 $("a[href='#top']").click(function() {
202 202 $("html, body").animate({ scrollTop: 0 }, "slow");
203 203 return false;
204 204 });
205 205
206 206 addImgPreview();
207 207
208 208 addRefLinkPreview();
209 209
210 210 highlightCode($(document));
211 211
212 212 initFavPanel();
213 213
214 214 var volumeUsers = $("video,audio");
215 215 processVolumeUser(volumeUsers);
216 216
217 217 addContextMenu();
218 218
219 219 compatibilityCrutches();
220 220 });
@@ -1,151 +1,151 b''
1 1 from django.core.urlresolvers import reverse
2 2 from django.shortcuts import render
3 3
4 4 from boards import settings
5 5 from boards.abstracts.paginator import get_paginator
6 6 from boards.abstracts.settingsmanager import get_settings_manager
7 7 from boards.models import Post
8 8 from boards.views.base import BaseBoardView
9 9 from boards.views.posting_mixin import PostMixin
10 10
11 11 POSTS_PER_PAGE = settings.get_int('View', 'PostsPerPage')
12 12
13 13 PARAMETER_CURRENT_PAGE = 'current_page'
14 14 PARAMETER_PAGINATOR = 'paginator'
15 15 PARAMETER_POSTS = 'posts'
16 16 PARAMETER_QUERIES = 'queries'
17 17
18 18 PARAMETER_PREV_LINK = 'prev_page_link'
19 19 PARAMETER_NEXT_LINK = 'next_page_link'
20 20
21 21 TEMPLATE = 'boards/feed.html'
22 22 DEFAULT_PAGE = 1
23 23
24 24
25 25 class FeedFilter:
26 26 @staticmethod
27 27 def get_filtered_posts(request, posts):
28 28 return posts
29 29
30 30 @staticmethod
31 31 def get_query(request):
32 32 return None
33 33
34 34
35 35 class TripcodeFilter(FeedFilter):
36 36 @staticmethod
37 37 def get_filtered_posts(request, posts):
38 38 filtered_posts = posts
39 39 tripcode = request.GET.get('tripcode', None)
40 40 if tripcode:
41 41 filtered_posts = filtered_posts.filter(tripcode=tripcode)
42 42 return filtered_posts
43 43
44 44 @staticmethod
45 45 def get_query(request):
46 46 tripcode = request.GET.get('tripcode', None)
47 47 if tripcode:
48 48 return 'Tripcode: {}'.format(tripcode)
49 49
50 50
51 51 class FavoritesFilter(FeedFilter):
52 52 @staticmethod
53 53 def get_filtered_posts(request, posts):
54 54 filtered_posts = posts
55 55
56 56 favorites = 'favorites' in request.GET
57 57 if favorites:
58 58 settings_manager = get_settings_manager(request)
59 59 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
60 60 fav_threads = [op.get_thread() for op in fav_thread_ops]
61 61 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
62 62 return filtered_posts
63 63
64 64
65 65 class IpFilter(FeedFilter):
66 66 @staticmethod
67 67 def get_filtered_posts(request, posts):
68 68 filtered_posts = posts
69 69
70 70 ip = request.GET.get('ip', None)
71 71 if ip and request.user.has_perm('post_delete'):
72 72 filtered_posts = filtered_posts.filter(poster_ip=ip)
73 73 return filtered_posts
74 74
75 75 @staticmethod
76 76 def get_query(request):
77 77 ip = request.GET.get('ip', None)
78 78 if ip:
79 79 return 'IP: {}'.format(ip)
80 80
81 81
82 class HashFilter(FeedFilter):
82 class ImageFilter(FeedFilter):
83 83 @staticmethod
84 84 def get_filtered_posts(request, posts):
85 85 filtered_posts = posts
86 86
87 image_hash = request.GET.get('image_hash', None)
88 if image_hash:
89 filtered_posts = filtered_posts.filter(attachments__hash=image_hash)
87 image = request.GET.get('image', None)
88 if image:
89 filtered_posts = filtered_posts.filter(attachments__file=image)
90 90 return filtered_posts
91 91
92 92 @staticmethod
93 93 def get_query(request):
94 image_hash = request.GET.get('image_hash', None)
95 if image_hash:
96 return 'Hash: {}'.format(image_hash)
94 image = request.GET.get('image', None)
95 if image:
96 return 'File: {}'.format(image)
97 97
98 98
99 99 class FeedView(PostMixin, BaseBoardView):
100 100 filters = (
101 101 TripcodeFilter,
102 102 FavoritesFilter,
103 103 IpFilter,
104 HashFilter,
104 ImageFilter,
105 105 )
106 106
107 107 def get(self, request):
108 108 page = request.GET.get('page', DEFAULT_PAGE)
109 109
110 110 params = self.get_context_data(request=request)
111 111
112 112 settings_manager = get_settings_manager(request)
113 113
114 114 posts = Post.objects.exclude(
115 115 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
116 116 '-pub_time').prefetch_related('attachments', 'thread')
117 117 queries = []
118 118 for filter in self.filters:
119 119 posts = filter.get_filtered_posts(request, posts)
120 120 query = filter.get_query(request)
121 121 if query:
122 122 queries.append(query)
123 123 params[PARAMETER_QUERIES] = queries
124 124
125 125 paginator = get_paginator(posts, POSTS_PER_PAGE)
126 126 paginator.current_page = int(page)
127 127
128 128 params[PARAMETER_POSTS] = paginator.page(page).object_list
129 129
130 130 paginator.set_url(reverse('feed'), request.GET.dict())
131 131
132 132 self.get_page_context(paginator, params, page)
133 133
134 134 return render(request, TEMPLATE, params)
135 135
136 136 # TODO Dedup this into PagedMixin
137 137 def get_page_context(self, paginator, params, page):
138 138 """
139 139 Get pagination context variables
140 140 """
141 141
142 142 params[PARAMETER_PAGINATOR] = paginator
143 143 current_page = paginator.page(int(page))
144 144 params[PARAMETER_CURRENT_PAGE] = current_page
145 145 if current_page.has_previous():
146 146 params[PARAMETER_PREV_LINK] = paginator.get_page_url(
147 147 current_page.previous_page_number())
148 148 if current_page.has_next():
149 149 params[PARAMETER_NEXT_LINK] = paginator.get_page_url(
150 150 current_page.next_page_number())
151 151
General Comments 0
You need to be logged in to leave comments. Login now