OSDN Git Service

動画拡張子の予測を改良
[pynv/niconico-memories-gui.git] / niconico-memories-gui.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3
4 #       Copyright © 2014 dyknon
5 #
6 #       This file is part of NicoNicoMemories GUI.
7 #
8 #       NicoNicoMemories GUI is free software: you can redistribute it and/or modify
9 #       it under the terms of the GNU General Public License as published by
10 #       the Free Software Foundation, either version 3 of the License, or
11 #       (at your option) any later version.
12 #
13 #       This program is distributed in the hope that it will be useful,
14 #       but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #       GNU General Public License for more details.
17 #
18 #       You should have received a copy of the GNU General Public License
19 #       along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21 import tkinter
22 import tkinter.messagebox
23 import threading
24 import nicovideo.access
25 import nicovideo.comment
26 import nicovideo.tools
27 import nicovideo.logger
28 import locale
29 import mimetypes
30 import time
31 import re
32
33 root = tkinter.Tk()
34 quelock = threading.Lock()
35 que_list = []
36 next_queid = 0
37 convert_thread = None
38
39 midfinder = re.compile(r"^\s*(?:(?:(?:(?:(?:http:)?//?)?www.nicovideo.jp)?/)?watch/)?([a-z]{2}[0-9]+)(?:\?.*)?\s*$")
40 midfinder2 = re.compile(r"^\s*(?:(?:(?:(?:http:)?//?)?www.nicovideo.jp)?/)?watch/((?:[a-z]{2})?[0-9]+)(?:\?.*)?\s*$")
41
42 def add_que(request, dl_video):
43         global next_queid
44
45         mo1 = midfinder.search(request)
46         mo2 = midfinder.search(request)
47         videoid = None
48         if mo1:
49                 videoid = mo1.group(1)
50         elif mo2:
51                 videoid = mo2.group(1)
52
53         newque = {}
54         if videoid == None:
55                 newque["videoid"] = request
56                 newque["stat"] = "iderr"
57                 newque["title"] = ""
58         else:
59                 newque["videoid"] = videoid
60                 newque["dl_video"] = dl_video
61                 m = nicovideo.access.Video(videoid)
62                 try:
63                         m.get_standard_info()
64                 except nicovideo.err.NotFound:
65                         newque["stat"] = "notfound"
66                         newque["title"] = ""
67                 else:
68                         newque["m"] = m
69                         newque["stat"] = "wait"
70                         newque["title"] = m.title
71
72         quelock.acquire()
73         newque["id"] = next_queid
74         que_list.append(newque)
75         next_queid += 1
76         quelock.release()
77
78 def do_thread():
79         while True:
80                 quelock.acquire()
81                 for que in que_list:
82                         if que["stat"] == "wait":
83                                 quelock.release()
84                                 try:
85                                         do_convert(que)
86                                 except FileNotFoundError:
87                                         que["stat"] = "エラー:ファイルが見つかりませんでした"
88                                 break
89                 else:
90                         quelock.release()
91                 time.sleep(1)
92
93 def do_convert(que):
94         def display():
95                 quelock.acquire()
96                 que["stat"] = stat + " " + prog
97                 quelock.release()
98
99         m = que["m"]
100
101         fnf = nicovideo.tools.filename_fixer
102
103         stat = "ダウンロードを開始します"
104         prog = ""
105         display()
106
107         m.get_standard_info()
108         filename = m.title + "[" + m.watch_id + "]"
109         system_encoding = locale.getpreferredencoding()
110         filename = filename.encode(system_encoding, "replace")
111         filename = filename.decode(system_encoding, "replace")
112
113         stat = "コメントにアクセスしています"
114         display()
115         cmfn = fnf(filename + ".xml")
116         conn = m.generate_connection_to_coments()
117         res = conn.getresponse()
118         if res.status == 200:
119                 try:
120                         filehandle = open(cmfn, mode="wb")
121                 except IOError:
122                         prog = "コメントファイルを作れません"
123                         display()
124                         return
125                 filesize = int(res.getheader("Content-Length", default=-1))
126                 stat = "コメントダウンロード中"
127                 prog = "0%"
128                 display()
129                 sizedled = 0
130                 lastprint = -1
131                 while True:
132                         buf = res.read(1024)
133                         sizedled += len(buf)
134                         nowtime = int(time.time())
135                         if lastprint != nowtime:
136                                 lastprint = nowtime
137                                 if filesize >= 0:
138                                         pst = int(sizedled / filesize * 100 + 0.5)
139                                         prog = "{}%".format(pst)
140                                 else:
141                                         prog = "{}byte".format(sizedled)
142                                 display()
143                         if len(buf) == 0:
144                                 break
145                         filehandle.write(buf)
146         else:
147                 stat = "コメントのDLに失敗しました。"
148                 display()
149                 return
150
151         if que["dl_video"]:
152                 stat = "動画ファイルにアクセスしています"
153                 display()
154                 conn = m.generate_connection_to_video()
155                 res = conn.getresponse()
156                 if res.status == 200:
157                         mimetype = res.getheader("Content-Type", default="unknown")
158                         filename_ext = mimetypes.guess_extension(mimetype)
159                         if not filename_ext:
160                                 filename_ext = "." + mimetype[mimetype.find("/")+1:]
161                         filename_tail = ""
162                         if m.info["play_video_url"][len(m.info["play_video_url"])-3:] \
163                                         == "low":
164                                 filename_tail += ""
165                         filename_tail += filename_ext
166                         try:
167                                 filehandle = open(fnf(filename+filename_tail), mode="wb")
168                         except IOError:
169                                 prog = "動画ファイルを作れません"
170                                 display()
171                                 return
172                         filesize = int(res.getheader("Content-Length", default=-1))
173                         stat = "動画ファイルダウンロード中"
174                         prog = "0%".format(filesize)
175                         display()
176                         sizedled = 0
177                         lastprint = -1
178                         while True:
179                                 buf = res.read(1024)
180                                 sizedled += len(buf)
181                                 nowtime = int(time.time())
182                                 if lastprint != nowtime:
183                                         lastprint = nowtime
184                                         if filesize >= 0:
185                                                 pst = int(sizedled / filesize * 100 + 0.5)
186                                                 prog = "{}%".format(pst)
187                                         else:
188                                                 prog = "{}byte".format(sizedled)
189                                         display()
190                                 if len(buf) == 0:
191                                         break
192                                 filehandle.write(buf)
193                 else:
194                         stat = "動画ファイルのDLに失敗しました。"
195                         display()
196                         return
197
198         stat = "コメントを字幕化しています"
199         display()
200         if not "playerinfo" in m.info_downloaded:
201                 m.get_player_info()
202         if "is_wide" in m.info and int(m.info["is_wide"]):
203                 vs = nicovideo.comment.VirtualScreen("16:9")
204         else:
205                 vs = nicovideo.comment.VirtualScreen("4:3")
206         prog = "xml読み込み中"
207         display()
208         try:
209                 filehandle = open(cmfn, mode="rb")
210         except IOError:
211                 prog = "コメントファイルのオープンに失敗しました。"
212                 display()
213                 return
214         x = nicovideo.comment.interpret_xml(filehandle)
215         vs.add(x["threads"])
216         filehandle = open(fnf(filename + ".ass"), mode="wb")
217         prog = "ass化中"
218         display()
219         filehandle.write(vs.get_ass().encode("UTF-8", "replace"))
220
221         quelock.acquire()
222         que["stat"] = "finish"
223         quelock.release()
224
225 class NamedInputBox(tkinter.Frame):
226         def __init__(self, master=None, name="", value="", width=20):
227                 tkinter.Frame.__init__(self, master)
228                 self.pack()
229                 self.value = tkinter.StringVar()
230                 self.value.set(value)
231                 self.lab = tkinter.Label(self, text=name)
232                 self.lab.pack(side="left", fill="both")
233                 self.ent = tkinter.Entry(self, textvariable=self.value, width=width)
234                 self.ent.pack(side="left", fill="both")
235
236 class PreviewItem(tkinter.Frame):
237         def __init__(self, master):
238                 tkinter.Frame.__init__(self, master)
239                 self.idlabel = tkinter.Label(self, text="")
240                 self.messagelabel = tkinter.Label(self, text="")
241                 self.titlelabel = tkinter.Label(self, text="")
242                 self.idlabel.grid(column=0, row=0, sticky="w")
243                 self.messagelabel.grid(column=1, row=0, sticky="e")
244                 self.titlelabel.grid(column=0, row=1, columnspan=2)
245                 self.columnconfigure(self.titlelabel, weight=1)
246
247         def update(self, status):
248                 self.idlabel["text"] = status["id"]
249                 self.messagelabel["text"] = status["message"]
250                 self.titlelabel["text"] = status["title"]
251
252         def set_color(self, color):
253                 self["background"] = color
254                 self.idlabel["background"] = color
255                 self.messagelabel["background"] = color
256                 self.titlelabel["background"] = color
257
258 class PreviewArea(tkinter.LabelFrame):
259         def __init__(self, master, label):
260                 tkinter.LabelFrame.__init__(self, master, text=label, pady=2, padx=2)
261                 self.preview = tkinter.Canvas(self)
262                 self.scr_y = tkinter.Scrollbar(self)
263                 self.scr_x = tkinter.Scrollbar(self)
264                 self.preview["width"] = 500
265                 self.preview["height"] = 200
266                 self.preview["bg"] = "white"
267                 self.preview["xscrollcommand"] = self.scr_x.set
268                 self.preview["yscrollcommand"] = self.scr_y.set
269                 self.scr_x["orient"] = "horizontal"
270                 self.scr_x["command"] = self.preview.xview
271                 self.scr_y["orient"] = "vertical"
272                 self.scr_y["command"] = self.preview.yview
273                 self.preview.grid(row=0, column=0, sticky="we")
274                 self.scr_x.grid(row=1, column=0, sticky="we")
275                 self.scr_y.grid(row=0, column=1, sticky="ns")
276                 self.preview["scrollregion"] = self.preview.bbox("all")
277                 self.idlist = []
278
279         def update_state(self):
280                 quelock.acquire()
281                 for que in que_list:
282                         status = {"id": que["videoid"]}
283                         status["title"] = que["title"]
284                         if que["stat"] == "wait":
285                                 status["type"] = "wait"
286                                 status["message"] = "順番待ち中です。"
287                         elif que["stat"] == "notfound":
288                                 status["type"] = "error"
289                                 status["message"] = "動画がありません。"
290                         elif que["stat"] == "iderr":
291                                 status["type"] = "error"
292                                 status["message"] = "動画ID検出失敗"
293                         elif que["stat"] == "finish":
294                                 status["type"] = "finish"
295                                 status["message"] = "完了。"
296                         elif que["stat"] == "error":
297                                 status["type"] = "error"
298                                 status["message"] = "何らかの問題が発生しました。"
299                         else:
300                                 status["type"] = "prog"
301                                 status["message"] = que["stat"]
302                         for disp in self.idlist:
303                                 if que["id"] == disp["queid"]:
304                                         disp["status"] = status
305                                         break
306                         else:
307                                 newdisp = {}
308                                 newdisp["queid"] = que["id"]
309                                 newdisp["status"] = status
310                                 newdisp["item"] = PreviewItem(self)
311                                 newdisp["id"] = self.preview.create_window(0, 0,
312                                         window=newdisp["item"],
313                                         anchor="nw",
314                                         width=500,
315                                         height=40
316                                 )
317                                 self.idlist[0:0] = [newdisp]
318                 quelock.release()
319                 ktn = 0
320                 colortable = ["#ddddff", "#ddffdd"]
321                 colorindex = 0
322                 for disp in self.idlist:
323                         disp["item"].update(disp["status"])
324                         disp["item"].set_color(colortable[colorindex])
325                         self.preview.coords(disp["id"], (0, ktn))
326                         ktn = self.preview.bbox(disp["id"])[3]
327                         colorindex += 1
328                         if len(colortable) <= colorindex:
329                                 colorindex = 0
330                         pass
331                 self.preview["scrollregion"] = self.preview.bbox("all")
332                 self.after(10, self.update_state)
333
334 class NnmGui(tkinter.Frame):
335         def __init__(self, master):
336                 tkinter.Frame.__init__(self, master)
337                 self.flag_dl_video = tkinter.BooleanVar()
338                 self.id_box = NamedInputBox(self, "動画ID:", "", 80)
339                 self.id_box.bind_all("<Key-Return>", self.do_conv)
340                 self.id_box.pack(side="top", fill="both")
341                 self.go_frame = tkinter.Frame(self)
342                 self.go_frame.pack(side="top", fill="both")
343                 self.do_button = tkinter.Button(self.go_frame)
344                 self.do_button["text"] = "GO!"
345                 self.do_button["command"] = self.do_conv
346                 self.do_button.pack(side="left", fill="both")
347                 self.videodl_check = tkinter.Checkbutton(self.go_frame)
348                 self.videodl_check["text"] = "動画をDLする"
349                 self.videodl_check["variable"] = self.flag_dl_video
350                 self.videodl_check.pack(side="left", fill="both")
351                 self.preview_area = PreviewArea(self, "一覧")
352                 self.preview_area.pack(side="top")
353                 self.preview_area.update_state()
354         
355         def do_conv(self, event=None):
356                 videoid = self.id_box.value.get()
357                 dl_video = self.flag_dl_video.get()
358                 self.subthread = threading.Thread(      target=add_que,
359                                                                                         args=(videoid, dl_video))
360                 self.subthread.daemon = True
361                 self.subthread.start()
362         
363         def fin_conv(self):
364                 self.do_button["text"] = "GO!"
365
366 app = NnmGui(root)
367 app.pack()
368 root.title("NicoNicoMemories GUI")
369 do_thread_obj = threading.Thread(target=do_thread)
370 do_thread_obj.daemon = True
371 do_thread_obj.start()
372 app.mainloop()