# Tauon Music Box - URL stream download and encoding module

# Copyright © 2020, Taiko2k captain(dot)gxj(at)gmail.com

#     This file is part of Tauon Music Box.
#
#     Tauon Music Box 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.
#
#     Tauon Music Box 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 Lesser General Public License for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with Tauon Music Box.  If not, see <http://www.gnu.org/licenses/>.
import mimetypes
import urllib.request
import threading
import time
import subprocess
import os
import sys
if sys.platform != 'win32':
    import fcntl
import datetime
import io
import shutil
import mutagen
import copy
from PIL import Image
from mutagen.flac import Picture
import base64
from t_modules.t_extra import filename_safe
from t_modules.t_webserve import vb

class StreamEnc:

    def __init__(self, tauon):
        self.tauon = tauon
        self.download_running = False
        self.encode_running = False
        self.pump_running = False
        self.feed_running = False

        self.download_process = False
        self.abort = False

        self.s_name = ""
        self.s_bitrate = ""
        self.s_genre = ""
        self.s_description = ""
        self.s_mime = ""
        self.s_format = ""

        self.chunks = {}
        self.c = 0
        self.url = None

    def stop(self):

        try:
            self.tauon.radiobox.websocket.close()
            print("Websocket closed")
        except:
            print("No socket to close?")

        self.abort = True
        self.tauon.radiobox.loaded_url = None

    def start_download(self, url):

        self.abort = True
        while self.download_running:
            time.sleep(0.01)
        while self.encode_running:
            time.sleep(0.01)
        while self.pump_running:
            time.sleep(0.01)

        self.__init__(self.tauon)

        self.url = url
        result = self.start_request()
        if not result:
            return False

        self.download_process = threading.Thread(target=self.pump)
        self.download_process.daemon = True
        self.download_process.start()
        return True


    def start_request(self):
        url = self.url
        def NiceToICY(self):
            class InterceptedHTTPResponse:
                pass
            if not url.endswith(".ts"):
                line = self.fp.readline().replace(b"ICY 200 OK\r\n", b"HTTP/1.0 200 OK\r\n")
            else:
                line = self.fp.readline()
            InterceptedSelf = InterceptedHTTPResponse()
            InterceptedSelf.fp = io.BufferedReader(io.BytesIO(line))
            InterceptedSelf.debuglevel = self.debuglevel
            InterceptedSelf._close_conn = self._close_conn
            return ORIGINAL_HTTP_CLIENT_READ_STATUS(InterceptedSelf)

        ORIGINAL_HTTP_CLIENT_READ_STATUS = urllib.request.http.client.HTTPResponse._read_status
        urllib.request.http.client.HTTPResponse._read_status = NiceToICY

        retry = 5
        while True:
            try:
                r = urllib.request.Request(self.url)
                #r.add_header('GET', '1')
                if not self.url.endswith(".ts"):
                    r.add_header('Icy-MetaData', '1')
                r.add_header('User-Agent', self.tauon.t_agent)
                print("Open URL.....")
                r = urllib.request.urlopen(r, timeout=20, cafile=self.tauon.ca)
                print("URL opened.")

            except Exception as e:

                print("URL error...")
                retry -= 1
                if retry > 0 and "Temporary" in str(e):
                    time.sleep(2)
                    print("RETRYING...")
                    continue
                else:
                    print("Connection failed")
                    print(str(e))
                    self.tauon.gui.show_message("Failed to establish a connection", str(e), mode="error")
                    return False
            break

        self.download_process = threading.Thread(target=self.run_download, args=([r]))
        self.download_process.daemon = True
        self.download_process.start()
        self.download_running = True
        return True

    def pump(self):
        aud = self.tauon.aud
        if self.tauon.prefs.backend != 4 or not aud:
            print("Radio error: Phazor not loaded")
            return
        self.pump_running = True

        rate = str(self.tauon.prefs.samplerate)
        cmd = [self.tauon.get_ffmpeg(), "-loglevel", "quiet", "-i", "pipe:0", "-acodec", "pcm_s16le", "-f", "s16le", "-ac", "2", "-ar",
               rate, "-"]

        startupinfo = None
        if self.tauon.msys:
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        decoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, startupinfo=startupinfo)
        if sys.platform != 'win32':
           fcntl.fcntl(decoder.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)

        raw_audio = None
        max_read = int(10000)
        vb.reset()
        vb.tauon = self.tauon

        def feed(decoder):
            position = 0
            self.feed_running = True
            try:
                while True:
                    if position < self.tauon.stream_proxy.c:
                        if position not in self.tauon.stream_proxy.chunks:
                            print("The buffer was deleted too soon!")
                            break

                        chunk = self.chunks[position]
                        decoder.stdin.write(chunk)
                        vb.input(self.tauon.stream_proxy.chunks[position])
                        position += 1
                    else:
                        time.sleep(0.01)
                    if self.abort or not self.pump_running or not self.feed_running:
                        break
            except:
                self.feed_running = False
                raise
            print("Exit feeder")

        feeder = threading.Thread(target=feed, args=[decoder])
        feeder.daemon = True
        feeder.start()

        retry = 3

        while True:
            if not self.tauon.stream_proxy.download_running or self.abort:
                break
            if raw_audio is None:
                raw_audio = decoder.stdout.read(max_read)
            if raw_audio:
                r = aud.feed_ready(max_read)
                if r:
                    aud.feed_raw(len(raw_audio), raw_audio)
                    if len(raw_audio) < max_read:
                        time.sleep(0.01)
                    raw_audio = None
                    continue

            time.sleep(0.01)


        decoder.terminate()
        time.sleep(0.1)
        try:
            decoder.kill()
        except:
            pass

        self.pump_running = False


    def encode(self):

        self.encode_running = True

        try:

            while self.c < 20:
                if self.abort:
                    self.encode_running = False
                    return
                time.sleep(0.05)

            ext = ".opus"
            rate = "48000"
            codec = self.tauon.prefs.radio_record_codec.upper()
            if codec == "OGG":
                ext = ".ogg"
                rate = "44100"
            if codec == "MP3":
                ext = ".mp3"
                rate = "44100"
            if codec == "FLAC":
                ext = ".flac"
                rate = "44100"

            target_file = os.path.join(self.tauon.cache_directory, "stream" + ext)
            if os.path.isfile(target_file):
                os.remove(target_file)

            cmd = [self.tauon.get_ffmpeg(), "-loglevel", "quiet", "-i", "pipe:0", "-acodec", "pcm_s16le", "-f", "s16le", "-ac", "2", "-ar", rate, "-"]

            decoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
            if sys.platform != 'win32':
                fcntl.fcntl(decoder.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)

            position = 0
            old_metadata = self.tauon.radiobox.song_key
            old_tags = self.tauon.pctl.found_tags

            ##cmd = ["opusenc", "--raw", "--raw-rate", "48000", "-", target_file]
            cmd = ["ffmpeg", "-loglevel", "quiet", "-f", "s16le", "-ar", rate, "-ac", "2", "-i", "pipe:0", target_file]
            encoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)

            def save_track():
                #self.tauon.recorded_songs.append(song)

                save_file = '{:%Y-%m-%d %H-%M-%S} - '.format(datetime.datetime.now())
                save_file += filename_safe(old_metadata)
                save_file = save_file.strip() + ext
                save_file = os.path.join(self.tauon.prefs.encoder_output, save_file)
                if os.path.exists(save_file):
                    os.remove(save_file)
                if not os.path.exists(self.tauon.prefs.encoder_output):
                    os.makedirs(self.tauon.prefs.encoder_output)
                shutil.move(target_file, save_file)

                # print(self.tauon.pctl.tag_history)
                # print(old_metadata)
                tags = self.tauon.pctl.tag_history.get(old_metadata, None)
                if tags:
                    print("Save metadata to file")
                    #print(tags)
                    muta = mutagen.File(save_file, easy=True)
                    muta["artist"] = tags.get("artist", "")
                    muta["title"] = tags.get("title", "")
                    muta["album"] = tags.get("album", "")
                    # if tags["image"]:
                    #     tags["image"].seek(0)
                    #     im = Image.open(tags["image"])
                    #     width, height = im.size
                    #     tags["image"].seek(0)
                    #
                    #     picture = Picture()
                    #     tags["image"].seek(0)
                    #     picture.data = tags["image"].read()
                    #     picture.type = 3
                    #     picture.desc = ""
                    #     picture.mime = "image/jpeg"
                    #     picture.width = width
                    #     picture.height = height
                    #
                    #     mode_to_bpp = {'1': 1, 'L': 8, 'P': 8, 'RGB': 24, 'RGBA': 32, 'CMYK': 32, 'YCbCr': 24, 'I': 32,
                    #                    'F': 32}
                    #     picture.depth = mode_to_bpp[im.mode]
                    #
                    #     picture_data = picture.write()
                    #     encoded_data = base64.b64encode(picture_data)
                    #     vcomment_value = encoded_data.decode("ascii")
                    #     muta["metadata_block_picture"] = [vcomment_value]

                    muta.save()

                target_pl = None
                for i, pl in enumerate(self.tauon.pctl.multi_playlist):
                    if pl[0] == "Saved Radio Tracks":
                        target_pl = i
                if target_pl is None:
                    self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title="Saved Radio Tracks"))
                    target_pl = len(self.tauon.pctl.multi_playlist) - 1

                load_order = self.tauon.pctl.LoadClass()
                load_order.playlist = self.tauon.pctl.multi_playlist[target_pl][6]
                load_order.target = save_file
                self.tauon.load_orders.append(copy.deepcopy(load_order))
                self.tauon.gui.update += 1

            while True:

                if self.abort:
                    decoder.terminate()
                    encoder.terminate()
                    time.sleep(0.1)
                    try:
                        decoder.kill()
                    except:
                        pass
                    try:
                        encoder.kill()
                    except:
                        pass

                    if os.path.exists(target_file):
                        if os.path.getsize(target_file) > 256000:
                            print("Save file")
                            save_track()
                        else:
                            print("Discard small file")
                            os.remove(target_file)

                    self.encode_running = False
                    self.tauon.pctl.tag_history.clear()
                    return

                if old_metadata != self.tauon.radiobox.song_key:
                    if self.c < 400 and not old_metadata:
                        old_metadata = self.tauon.radiobox.song_key
                    elif not os.path.exists(target_file) or os.path.getsize(target_file) < 100000:
                        old_metadata = self.tauon.radiobox.song_key
                    else:
                        print("Split and save file")
                        encoder.stdin.close()
                        try:
                            encoder.wait(timeout=4)
                        except:
                            pass
                        try:
                            encoder.kill()
                        except:
                            pass
                        if os.path.exists(target_file):
                            if os.path.getsize(target_file) > 256000:
                                save_track()
                            else:
                                print("Discard small file")
                                os.remove(target_file)
                        encoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)

                raw_audio = decoder.stdout.read(1000000)
                if raw_audio:
                    encoder.stdin.write(raw_audio)

                if position < self.c:
                    chunk = self.chunks[position]
                    position += 1
                    decoder.stdin.write(chunk)
                else:
                    time.sleep(0.005)

        except Exception as e:
            print("Encoder thread crashed!")
            print(str(e))
            #raise
            self.encode_running = False
            return

    def run_download(self, r):

        h = r.info()

        self.s_name = h.get("icy-name")
        metaint = h.get("icy-metaint")
        self.s_bitrate = h.get("icy-br")
        self.s_genre = h.get("icy-genre")
        self.s_description = h.get("icy-description")
        self.s_mime = h.get("Content-Type")

        print(self.s_mime)
        if self.s_mime == "audio/mpeg":
            self.s_format = "MP3"
        if self.s_mime == "audio/ogg":
            self.s_format = "OGG"
        if self.s_mime == "audio/aac":
            self.s_format = "AAC"
        if self.s_mime == "audio/aacp":
            self.s_format = "AAC+"

        test_done = 0

        icy = False
        m_remain = 0
        m = 0
        if metaint and int(metaint) > 0:
            m = int(metaint)
            m_remain = m
            icy = True

        maybe = b""

        if self.tauon.prefs.auto_rec:
            self.download_process = threading.Thread(target=self.encode)
            self.download_process.daemon = True
            self.download_process.start()

        try:
            while True:

                chunk = r.read(256)

                if self.abort:
                    r.close()
                    print("Abort stream connection")
                    self.download_running = False
                    return

                if chunk:
                    if not icy or m_remain > len(chunk):
                        # We're sure its data Its data, send it on
                        self.chunks[self.c] = chunk

                        # Delete old data
                        d = self.c - 90000
                        if d in self.chunks:
                            del self.chunks[d]

                        test_done += len(chunk)
                        self.c += 1
                        m_remain -= len(chunk)

                        continue
                    else:
                        # It may contain the metadata block, put it aside
                        maybe += chunk

                # Try to extract ICY tag
                if maybe:
                    data1 = maybe[:m_remain]
                    inter = maybe[m_remain:]

                    # Read the metadata length byte
                    if inter:
                        special = inter[0]
                        follow = special * 16

                        if len(inter) < follow + 2:
                            # Not enough data yet
                            continue

                        text = inter[1:follow + 1]
                        data2 = inter[follow + 1:]

                        self.chunks[self.c] = data1 + data2
                        # Delete old data
                        d = self.c - 90000
                        if d in self.chunks:
                            del self.chunks[d]

                        self.c += 1

                        test_done += len(data1)
                        test_done = 0

                        m_remain = m - len(data2)
                        test_done += len(data2)
                        maybe = b""

                        try:
                            meta = text.decode().rstrip("\x00")
                            for tag in meta.split(";"):
                                if '=' in tag:
                                    a, b = tag.split('=', 1)
                                    if a == 'StreamTitle':
                                        #print("Set meta")
                                        self.tauon.pctl.tag_meta = b.rstrip("'").lstrip("'")
                                        break
                        except:
                            r.close()
                            self.download_running = False
                            self.abort = True
                            self.tauon.gui.show_message("Data malformation detected. Stream aborted.", mode='error')
                            raise
        except Exception as e:
            print("Stream download thread crashed!")
            self.download_running = False
            self.abort = True
            return