# -*- coding: utf-8 -*-

#	Copyright © 2014 dyknon
#
#	This file is part of Pylib-nicovideo.
#
#	Pylib-nicovideo is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
注意事項
	これはニコニコ動画のAPIを利用して
	動画情報やらなにやらをお気楽に取得できるライブラリみたいなものです。
	ライセンスに従ってもらえれば、好きに使ってくれて構いませんし、
	これを参考にしてまた別の物を作ってくれるのも大歓迎です。
	しかし、使い方によってはサーバーに過大な負荷がかかる可能性もあります。
	また、快適さを追求するために、ログインしないで使用できるように
	なっているので、
	外部プレイヤーのための機能をいくらか使わせてもらっています。
	このソフトウェアを使って無秩序にニコニコ動画にアクセスすることは
	利用者が快適にサービスを利用できるように
	運営の方が用意してくださった外部プレイヤーを使って
	サーバーに負荷をかけることになります。
	そのようなことはなるべく、避けたいことですし、
	また、それによって外部プレイヤーの廃止や、
	以前のような利用制限がかけられてしまうようなことも起こらないように、
	使い方には十分気をつけてください。

	大事なことなので要約してもう一回書きます。
	サーバーに負荷がかかりすぎないよう、十分注意して使ってください。
"""

from . import err
from . import tools
from . import logger
import http.client
import urllib.parse
import xml.etree.ElementTree as XmlTree
import re
import io

class Video:
	"""ニコニコに投稿された動画にアクセスするためのClass"""
	def __init__(self, wid):
		self.watch_id = wid
		self.info_downloaded = {}
		self.info = {}
		self.title = None

	def url(self):
		return "http://www.nicovideo.jp/watch/" + self.watch_id

	def standard_info_url(self):
		return "http://ext.nicovideo.jp/api/getthumbinfo/" + self.watch_id

	def get_standard_info(self):
		#ニコニコ動画動画に関する基本的な情報を得ることのできる
		#有名なAPIであるhttp://ext.nicovideo.jp/api/getthumbinfo/を
		#利用して得ることのできる情報を取得します。
		#要修正:いくつか取得していない情報があります

		if "thumbinfo" in self.info_downloaded:
			return

		thumbinfo = {}

		thumbinfo["tags"] = []
		self.info["tags"] = thumbinfo["tags"]

		conn = http.client.HTTPConnection("ext.nicovideo.jp")
		conn.request("GET", "/api/getthumbinfo/" + self.watch_id)
		res = conn.getresponse()
		if res.status == 200:
			resdata = res.read()
			xmlroot = XmlTree.fromstring(resdata)
			if xmlroot.tag != "nicovideo_thumb_response":
				raise err.InterpretErr("ext.nicovideo.jp/api/getthumbinfo/")
			if xmlroot.attrib["status"] != "ok":
				errcode = "unknown"
				for child in xmlroot:
					if child.tag == "error":
						for cerror in child:
							if cerror.tag == "code":
								errcode = cerror.text
				raise err.NotFound(self.watch_id, errcode)
			if len(xmlroot) != 1 or \
					xmlroot[0].tag != "thumb":
				raise err.InterpretErr("ext.nicovideo.jp/api/getthumbinfo/")
			for info in xmlroot[0]:
				if info.tag == "video_id":
					self.info["videoid"] = info.text
				elif info.tag == "title":
					self.info["title"] = info.text
					self.title = info.text
				elif info.tag == "description":
					self.info["description"] = info.text
				elif info.tag == "thumbnail_url":
					self.info["thumbnail_url"] = info.text
				elif info.tag == "first_retrieve":
					self.info["upload_time"] = tools.parse_time(info.text)
				elif info.tag == "length":		#5:19
					if not "length" in self.info:
						spt = info.text.split(":")
						self.info["length"] = int(spt[0])*60 + int(spt[1])
				elif info.tag == "movie_type":	#flv
					self.info["movie_type"] = info.text
				elif info.tag == "size_high":	#21138631
					if not "videosize" in self.info:
						self.info["videosize"] = {}
					self.info["videosize"]["high"] = int(info.text)
				elif info.tag == "size_low":	#17436492
					if not "videosize" in self.info:
						self.info["videosize"] = {}
					self.info["videosize"]["low"] = int(info.text)
				elif info.tag == "view_counter":
					self.info["view_counter"] = int(info.text)
				elif info.tag == "comment_num":
					self.info["comment_num"] = int(info.text)
				elif info.tag == "mylist_counter":
					self.info["mylist_counter"] = int(info.text)
				elif info.tag == "last_res_body":	#comments_new
					self.info["last_res"] = info.text
				elif info.tag == "watch_url":
					pass
				elif info.tag == "thumb_type":	#video
					self.info["thumb_type"] = info.text
				elif info.tag == "embeddable":
					self.info["downloadable"] = int(info.text) != 0
				elif info.tag == "no_live_play":
					self.info["live_play"] = int(info.text) == 0
				elif info.tag == "tags":
					for tag_entry in info:
						if tag_entry.tag != "tag":
							next
						if "lock" in tag_entry.attrib:
							if int(tag_entry.attrib["lock"]):
								self.info["tags"]. \
									append([tag_entry.text, True])
							else:
								self.info["tags"]. \
									append([tag_entry.text, False])
						else:
							self.info["tags"].append([tag_entry.text, False])
				elif info.tag == "user_id":
					self.info["user_id"] = info.text
				thumbinfo[info.tag] = info.text
		else:
			conn.close()
			raise err.HttpErr(res.status, res.reason)
		conn.close()
		self.info_downloaded["thumbinfo"] = thumbinfo

	def get_player_info(self):
		#外部プレイヤーの呼び出しを行うJavascriptを解析(笑)して
		#動画情報を取得します。
		#ここで取得できる情報には、コメントや動画URLを特定する手がかりが
		#多く存在していますが、同時に何を意味するのかわからない情報も
		#多く存在するので改良の余地があります。

		if "playerinfo" in self.info_downloaded:
			return

		player_info = {}

		rawdata = tools.http_get_text_data("ext.nicovideo.jp", "/thumb_watch/" + self.watch_id)
		pattern_playerURL_finder = re.compile(r"^\s*Nicovideo\.playerUrl\s*=\s*'(.+)'\s*;\s*$")
		pattern_start_varvideo_finder = re.compile(r"^\s*var\s+video\s*=\s*new\s+Nicovideo\.Video\s*\(\s*\{\s*$")
		pattern_end_varvideo_finder = re.compile(r"^(.*)\}\s*\)\s*;\s*$")
		pattern_end_varplayer_finder = re.compile(r"^\s*}")
		pattern_item_interrupter = re.compile(r"^\s*(.+)\s*:\s*(.+)\s*$")
		pattern_item_interrupter2 = re.compile(r"^\s*'(.+)'\s*:\s*'(.+)'\s*$")
		stage = 0
		for line in rawdata.split("\n"):
			if stage == 0:
				mo = pattern_playerURL_finder.search(line)
				if mo:
					player_info["player_url"] = mo.group(1)
					self.info["player_url"] = mo.group(1)
					stage = 1
			elif stage == 1:
				if pattern_start_varvideo_finder.search(line):
					stage = 2
			elif stage == 2:
				mo = pattern_end_varvideo_finder.search(line)
				if mo:
					line = mo.group(1)
					stage = 3
				for item in line.split(","):
					mo = pattern_item_interrupter.search(item)
					if mo:
						name = mo.group(1)
						value = mo.group(2)
						if name == "v":
							self.info["v_value"] = tools.un_javascript_escape(value[1:-1])
						elif name == "id":
							self.info["videoid"] = tools.un_javascript_escape(value[1:-1])
						elif name == "title":
							self.info["title"] = tools.un_javascript_escape(value[1:-1])
							self.title = self.info["title"]
						elif name == "description":
							self.info["description"] = tools.un_javascript_escape(value[1:-1])
						elif name == "thumbnail":
							self.info["thumbnail_url"] = tools.un_javascript_escape(value[1:-1])
						elif name == "postedAt":
							pass			#面倒だから省略
						elif name == "length":
							self.info["length"] = int(value)
						elif name == "viewCount":
							self.info["view_counter"] = int(value)
						elif name == "mylistCount":
							self.info["mylist_counter"] = int(value)
						elif name == "commentCount":
							self.info["comment_num"] = int(value)
						elif name == "movieType":
							self.info["movie_type"] = tools.un_javascript_escape(value[1:-1])
						elif name == "isDeleted":
							self.info["is_deleted"] = (value == "true")
						elif name == "isMymemory":
							if value == "true":
								self.info["thumb_type"] = "mymemory"
							else:
								if not "thumb_type" in self.info:
									self.info["thumb_type"] = "video"
						player_info[name] = value
			elif stage == 3:
				stage = 4
			elif stage == 4:
				#変数Playerは場合によって要素数が変化し、
				#まだ未知の要素がある可能性もあるため、
				#すべての要素を保存しておきます。
				#また、あまり有用でないと思われる情報は
				#特別に変数を割り当てていないので、これらには
				#object.player_info[<要素名>] でアクセスしてください。
				if pattern_end_varplayer_finder.search(line):
					break
				for item in line.split(","):
					mo = pattern_item_interrupter2.search(item)
					if mo:
						name = tools.un_javascript_escape(mo.group(1))
						value = tools.un_javascript_escape(mo.group(2))
						if name == "thumbPlayKey":
							self.info["thumb_playkey"] = value
						elif name == "playerTimestamp":
							self.info["thumb_player_timestamp"] = value
						elif name == "language":
							self.info["language"] = value
						#以下の要素は存在しない場合も確認されていますが、
						#有用なため、個別に変数を割り当てます。
						#なお、真偽値についてはFalseの場合に
						#要素が存在しないと考えられます。
						elif name == "has_owner_thread":
							if int(value) == 0:
								self.info["comment_has_owner_thread"] = False
							else:
								self.info["comment_has_owner_thread"] = True
						elif name == "isWide":
							if int(value) == 0:
								self.info["is_wide"] = False
							else:
								self.info["is_wide"] = True
						player_info[name] = value
		else:
			raise err.InterpretErr("ext.nicovideo.jp/thumb_watch/")
		self.info_downloaded["playerinfo"] = player_info

	def get_play_info(self):
		#get_player_info()によって取得できた情報を元に、
		#動画ファイルへのアクセス権を獲得し、
		#動画ファイルのURLや、それにアクセスするための情報などを取得します。

		# === 注意 === #
		#この操作は本来
		#ニコニコ動画から提供されるプレイヤーが行うことなので、
		#連続アクセスなどでサーバーに負荷がかからないよう
		#特に注意してください。
		
		if "playinfo" in self.info_downloaded:
			return

		play_info = {}

		self.get_player_info()
		if	not "player_url" in self.info or \
			not "thumb_playkey" in self.info or \
			not "thumb_player_timestamp" in self.info:
			raise err.ApiUpdated("再生情報が十分に得られませんでした。")

		spurl = urllib.parse.urlsplit(self.info["player_url"])
		if spurl[0] != "http" or spurl[1] == "":
			raise err.ApiUpdated("外部プレイヤーURLがhttp://から始まっていない")
		conn = http.client.HTTPConnection(spurl[1])
		if spurl[3] == "":
			conn.request("GET", spurl[2])
		else:
			conn.request("GET", spurl[2] + "?" + spurl[3])
		res = conn.getresponse()
		if res.status != 200:
			conn.close()
			raise err.HttpErr(res.status, res.reason)
		conn.close()

		conn = http.client.HTTPConnection("ext.nicovideo.jp")
		conn.request("GET", "/swf/player/nicoplayer.swf?thumbWatch=1&ts=" + self.info["thumb_player_timestamp"])
		res = conn.getresponse()
		if res.status != 200:
			conn.close()
			raise err.HttpErr(res.status, res.reason)
		conn.close()

		postmes = "k=" + urllib.parse.quote_plus(self.info["thumb_playkey"]) + "&as3=1&v=" + urllib.parse.quote_plus(self.info["v_value"])
		logger.debug("play_info_req:\n" + postmes)
		conn = http.client.HTTPConnection("ext.nicovideo.jp")
		conn.request("POST", "/thumb_watch", body=postmes, headers={"Content-Length": len(postmes), "Content-Type": "application/x-www-form-urlencoded"})
		res = conn.getresponse()
		if res.status == 200:

			cookie_raw = res.getheader("Set-Cookie")
			if cookie_raw == None:
				raise ApiUpdated("responce of /thumb_watch doesn't include needed information")
			cookie = cookie_raw[:cookie_raw.find(";")]
			self.info["playinfo_cookie"] = cookie

			#このレスポンスは解析があまり進んでいないため、
			#意味の理解できていない要素も含めてすべて保存します。
			raw_body = res.read().decode("ascii", "replace")
			logger.debug("play_info:\n" + raw_body)
			for item in raw_body.split("&"):
				ind = item.find("=")
				if ind == -1:
					play_info[urllib.parse.unquote_plus(item)] = True
				else:
					play_info[urllib.parse.unquote_plus(item[:ind])] = urllib.parse.unquote_plus(item[ind+1:])
			if "thread_id" in play_info:
				self.info["play_thread_id"] = play_info["thread_id"]
			if "nicos_id" in play_info:
				self.info["play_nicos_id"] = play_info["nicos_id"]
			if "ms" in play_info:
				self.info["play_comments_url"] = play_info["ms"]
			if "url" in play_info:
				self.info["play_video_url"] = play_info["url"]
		else:
			conn.close()
			raise err.HttpErr(res.status, res.reason)
		conn.close()
		self.info_downloaded["playinfo"] = play_info

	def generate_connection_to_coments(self):
		#get_play_info()などで取得した情報を元に、
		#ニコニコ動画のプレイヤーがサーバーから取得しているものに近い
		#コメントの情報を取得するための
		#HTTPConnectionオブジェクトを生成し、
		#requestまで行ったものを返します。
		#つまり、getresponse()でレスポンスを得ることができる状態です。

		# === 注意 === #
		#この操作は本来
		#ニコニコ動画から提供されるプレイヤーが行うことなので、
		#連続アクセスなどでサーバーに負荷がかからないよう
		#特に注意してください。

		self.get_play_info()
		self.get_standard_info()

		if	not "play_thread_id" in self.info or \
			not "play_comments_url" in self.info or \
			not "length" in self.info:
			raise ApiUpdated("コメント取得に必要な情報が取得できませんでした")

		#コメントAPIに送信する情報には謎が多いので
		#解明され次第修正されるべきです
		if self.info["length"] < 60:
			comment_from = -100
		elif self.info["length"] < 300:
			comment_from = -250
		elif self.info["length"] < 600:
			comment_from = -500
		else:
			comment_from = -1000
		comment_from = "{}".format(comment_from)

		threads = ""

		#通常のコメント取得分
		#threads += '<thread thread="' + self.play_thread_id + '" version="20061206" res_from="' + comment_from + '" scores="1"/>'
		threads += '<thread thread="' + self.info["play_thread_id"] + '" version="20090904" res_from="' + comment_from + '" scores="1"/>'
		#時間帯ごとのコメント追加分(暫定処理,多分要修正)
		#要解析:このスレッドについてはリクエストの詳細がわかりません。
		#lvnは↑で取得した個数(これに含まれないものが取得できる)
		#minsは動画の長さ(分)を小数点以下切り捨てしたものですが、
		#これが正しい指定方法かは不明です。
		#レスポンスから推測すると、何かしらの間違いがあると思われます。
		lvn = "{}".format(-int(comment_from))
		mins = "{}".format(int(self.info["length"] / 60))
		threads += '<thread_leaves thread="' + self.info["play_thread_id"] + '" scores="1">0-' + mins + ':100,' + lvn + '</thread_leaves>'

		#ニコスクリプトによって投稿されたコメント?
		#詳細不明。
		if "play_nicos_id" in self.info:
			threads += '<thread thread="' + self.info["play_nicos_id"] + '" version="20090904" res_from="' + comment_from + '" scores="1"/>'
			threads += '<thread_leaves thread="' + self.info["play_nicos_id"] + '" scores="1">0-' + mins + ':100,' + lvn + '</thread_leaves>'
		
		#投稿者コメント
		if "comment_has_owner_thread" in self.info and \
			self.info["comment_has_owner_thread"]:
			threads += '<thread thread="' + self.info["play_thread_id"] + '" version="20090904" res_from="-1000" fork="1" click_revision="-1" scores="1"/>'

		postmes = "<packet>" + threads + "</packet>";
		spurl = urllib.parse.urlsplit(self.info["play_comments_url"])
		if spurl[0] != "http" or spurl[1] == "":
			raise ApiUpdated("Comment API URL doesn't start whith http://")
		conn = http.client.HTTPConnection(spurl[1])
		posthead = {
			"Content-Length": len(postmes),
			"Content-Type": "text/xml",
			"Cookie": self.info["playinfo_cookie"]
		}
		if spurl[3] == "":
			conn.request("POST", spurl[2], body=postmes, headers=posthead)
		else:
			conn.request("POST", spurl[2] + "?" + spurl[3], body=postmes, headers=posthead)
		return conn

	def download_comments_to_file(self, fileobj):
		#get_play_info()などで取得した情報を元に、
		#ニコニコ動画のプレイヤーがサーバーから取得しているものに近い
		#コメントの情報を取得し、fileobjに書き出します。
		#fileobjがio.TextIOBaseのサブクラスなら
		#文字列のデコードを行います。(非推奨)
		#そうでない場合はバイナリを書き込みます。

		# === 注意 === #
		#この操作は本来
		#ニコニコ動画から提供されるプレイヤーが行うことなので、
		#連続アクセスなどでサーバーに負荷がかからないよう
		#特に注意してください。

		conn = self.generate_connection_to_coments()
		res = conn.getresponse()
		if res.status == 200:
			if isinstance(fileobj, io.TextIOBase):
				#これくらいならいっぺんに書き込んでもいいかな?
				#というか、utf-8でエンコードされたものを
				#分割してデコードするなんてレベル高すぎて無理です
				fileobj.write(res.read().decode("utf-8", "replace"))
			else:
				#小分けにしたほうが負荷が少ないんじゃないかと思うけど
				#CPUに対する負荷はかえって増える気がする。(主に言語的に)
				#けど分けてみる
				while True:
					buf = res.read(2048)
					if len(buf) == 0:
						break
					fileobj.write(buf)
		else:
			conn.close()
			raise err.HttpErr(res.status, res.reason)
		conn.close()

	def generate_connection_to_video(self):
		#get_play_info()などで取得した情報を元に、
		#動画をダウンロードするための
		#HTTPConnectionオブジェクトを生成し、
		#requestまで行ったものを返します。
		#つまり、getresponse()でレスポンスを得ることができる状態です。

		# === 注意 === #
		#この操作は本来
		#ニコニコ動画から提供されるプレイヤーが行うことなので、
		#連続アクセスなどでサーバーに負荷がかからないよう
		#特に注意してください。

		self.get_play_info()
		if not "play_video_url" in self.info:
			raise ApiUpdated("動画DLに必要な情報を取得できませんでした。")

		spurl = urllib.parse.urlsplit(self.info["play_video_url"])
		if spurl[0] != "http" or spurl[1] == "":
			raise ApiUpdated("Video URL doesn't start whith http://")
		conn = http.client.HTTPConnection(spurl[1])
		gethead = {
			"Cookie": self.info["playinfo_cookie"]
		}
		if spurl[3] == "":
			conn.request("GET", spurl[2], headers=gethead)
		else:
			conn.request("GET", spurl[2] + "?" + spurl[3], headers=gethead)
		return conn

	def download_movie_to_file(self, fileobj):
		#get_play_info()などで取得した情報を元に、
		#動画をDLし、fileobjに書き出します。

		# === 注意 === #
		#この操作は本来
		#ニコニコ動画から提供されるプレイヤーが行うことなので、
		#連続アクセスなどでサーバーに負荷がかからないよう
		#特に注意してください。

		conn = self.generate_connection_to_video()
		res = conn.getresponse()
		if res.status == 200:
			while True:
				buf = res.read(2048)
				if len(buf) == 0:
					break
				fileobj.write(buf)
		else:
			conn.close()
			raise err.HttpErr(res.status, res.reason)
		conn.close()
