浏览代码

Apply complete rewrite from dev branch to master

Cammy 6 年之前
父节点
当前提交
6db3149863

+ 4 - 0
.gitignore

@@ -57,3 +57,7 @@ win_building/
 pildev\.ini
 
 pildev/
+
+\.idea/
+
+*.txt

+ 5 - 2
pyinstalive/__main__.py

@@ -1,4 +1,7 @@
-from .initialize import run
+try:  # Python 2
+    from startup import run
+except ImportError:  # Python 3
+    from .startup import run
 
 
 def main():
@@ -6,4 +9,4 @@ def main():
 
 
 if __name__ == '__main__':
-    run()
+    run()

+ 123 - 0
pyinstalive/assembler.py

@@ -0,0 +1,123 @@
+import os
+import shutil
+import re
+import glob
+import subprocess
+import json
+import sys
+try:
+    import pil
+    import logger
+    import helpers
+    from constants import Constants
+except ImportError:
+    from . import pil
+    from . import logger
+    from . import helpers
+    from .constants import Constants
+
+"""
+The content of this file was originally written by https://github.com/taengstagram
+The code has been edited for use in PyInstaLive.
+"""
+
+
+def _get_file_index(filename):
+    """ Extract the numbered index in filename for sorting """
+    mobj = re.match(r'.+\-(?P<idx>[0-9]+)\.[a-z]+', filename)
+    if mobj:
+        return int(mobj.group('idx'))
+    return -1
+
+
+def assemble(user_called=True):
+    try:
+        ass_json_file = pil.assemble_arg if pil.assemble_arg.endswith(".json") else pil.assemble_arg + ".json"
+        ass_mp4_file = os.path.join(pil.dl_path, os.path.basename(ass_json_file).replace("_downloads", "").replace(".json", ".mp4"))
+        ass_segment_dir = pil.assemble_arg.split('.')[0]
+        broadcast_info = {}
+        if not os.path.isdir(ass_segment_dir) or not os.listdir(ass_segment_dir):
+            logger.error('Segment directory does not exist or is empty: %s' % ass_segment_dir)
+            logger.separator()
+            return
+        if not os.path.isfile(ass_json_file):
+            logger.warn("No matching json file found for the segment directory, trying to continue without it.")
+            ass_stream_id = os.listdir(ass_segment_dir)[0].split('-')[0]
+            broadcast_info['id'] = ass_stream_id
+            broadcast_info['broadcast_status'] = "active"
+            broadcast_info['segments'] = {}
+        else:
+            with open(ass_json_file) as info_file:
+                broadcast_info = json.load(info_file)
+
+        if broadcast_info.get('broadcast_status', '') == 'post_live':
+            logger.error('Segments from replay downloads cannot be assembled.')
+            return
+
+        logger.info("Assembling video segments from folder: {}".format(ass_segment_dir))
+        stream_id = str(broadcast_info['id'])
+
+        segment_meta = broadcast_info.get('segments', {})
+        if segment_meta:
+            all_segments = [
+                os.path.join(ass_segment_dir, k)
+                for k in broadcast_info['segments'].keys()]
+        else:
+            all_segments = list(filter(
+                os.path.isfile,
+                glob.glob(os.path.join(ass_segment_dir, '%s-*.m4v' % stream_id))))
+
+        all_segments = sorted(all_segments, key=lambda x: _get_file_index(x))
+        sources = []
+        audio_stream_format = 'assembled_source_{0}_{1}_mp4.tmp'
+        video_stream_format = 'assembled_source_{0}_{1}_m4a.tmp'
+        video_stream = ''
+        audio_stream = ''
+        for segment in all_segments:
+
+            if not os.path.isfile(segment.replace('.m4v', '.m4a')):
+                logger.warn('Audio segment not found: {0!s}'.format(segment.replace('.m4v', '.m4a')))
+                continue
+
+            if segment.endswith('-init.m4v'):
+                logger.info('Replacing %s' % segment)
+                segment = os.path.join(
+                    os.path.dirname(os.path.realpath(__file__)), 'repair', 'init.m4v')
+
+            if segment.endswith('-0.m4v'):
+                continue
+
+            video_stream = os.path.join(
+                ass_segment_dir, video_stream_format.format(stream_id, len(sources)))
+            audio_stream = os.path.join(
+                ass_segment_dir, audio_stream_format.format(stream_id, len(sources)))
+
+
+            file_mode = 'ab'
+
+            with open(video_stream, file_mode) as outfile, open(segment, 'rb') as readfile:
+                shutil.copyfileobj(readfile, outfile)
+
+            with open(audio_stream, file_mode) as outfile, open(segment.replace('.m4v', '.m4a'), 'rb') as readfile:
+                shutil.copyfileobj(readfile, outfile)
+
+        if audio_stream and video_stream:
+            sources.append({'video': video_stream, 'audio': audio_stream})
+
+        for n, source in enumerate(sources):
+            ffmpeg_binary = os.getenv('FFMPEG_BINARY', 'ffmpeg')
+            cmd = [
+                ffmpeg_binary, '-loglevel', 'warning', '-y',
+                '-i', source['audio'],
+                '-i', source['video'],
+                '-c:v', 'copy', '-c:a', 'copy', ass_mp4_file]
+            fnull = open(os.devnull, 'w')
+            exit_code = subprocess.call(cmd, stdout=fnull, stderr=subprocess.STDOUT)
+            if exit_code != 0:
+                logger.warn("FFmpeg exit code not '0' but '{:d}'.".format(exit_code))
+            logger.separator()
+            logger.info('The video file has been generated: %s' % os.path.basename(ass_mp4_file))
+            if user_called:
+                logger.separator()
+    except Exception as e:
+        logger.error("An error occurred: {:s}".format(str(e)))

+ 99 - 127
pyinstalive/auth.py

@@ -4,142 +4,114 @@ import json
 import os.path
 import sys
 
-from .logger import log_seperator, supports_color, log_info_blue, log_info_green, log_warn, log_error, log_whiteline, log_plain
-
-
-
+try:
+    import logger
+    import helpers
+    import pil
+except ImportError:
+    from . import logger
+    from . import helpers
+    from . import pil
 
 try:
-	from instagram_private_api import (
-		Client, ClientError, ClientLoginError,
-		ClientCookieExpiredError, ClientLoginRequiredError)
+    from instagram_private_api import (
+        Client, ClientError, ClientLoginError,
+        ClientCookieExpiredError, ClientLoginRequiredError)
 except ImportError:
-	import sys
-	sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
-	from instagram_private_api import (
-		Client, ClientError, ClientLoginError,
-		ClientCookieExpiredError, ClientLoginRequiredError)
+    sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+    from instagram_private_api import (
+        Client, ClientError, ClientLoginError,
+        ClientCookieExpiredError, ClientLoginRequiredError)
 
 
 def to_json(python_object):
-	if isinstance(python_object, bytes):
-		return {'__class__': 'bytes',
-				'__value__': codecs.encode(python_object, 'base64').decode()}
-	raise TypeError(repr(python_object) + ' is not JSON serializable')
+    if isinstance(python_object, bytes):
+        return {'__class__': 'bytes',
+                '__value__': codecs.encode(python_object, 'base64').decode()}
+    raise TypeError(repr(python_object) + ' is not JSON serializable')
 
 
 def from_json(json_object):
-	if '__class__' in json_object and json_object.get('__class__') == 'bytes':
-		return codecs.decode(json_object.get('__value__').encode(), 'base64')
-	return json_object
+    if '__class__' in json_object and json_object.get('__class__') == 'bytes':
+        return codecs.decode(json_object.get('__value__').encode(), 'base64')
+    return json_object
 
 
 def onlogin_callback(api, cookie_file):
-	cache_settings = api.settings
-	with open(cookie_file, 'w') as outfile:
-		json.dump(cache_settings, outfile, default=to_json)
-		log_info_green('New cookie file was made: {0!s}'.format(cookie_file))
-		log_seperator()
-
-
-def login(username, password, show_cookie_expiry, force_use_login_args):
-	device_id = None
-	try:
-		if force_use_login_args:
-			log_info_blue("Overriding configuration file login with -u and -p arguments...")
-			log_seperator()
-		cookie_file = "{}.json".format(username)
-		if not os.path.isfile(cookie_file):
-			# settings file does not exist
-			log_warn('Unable to find cookie file: {0!s}'.format(cookie_file))
-			log_info_green('Creating a new cookie file...')
-
-			# login new
-			api = Client(
-				username, password,
-				on_login=lambda x: onlogin_callback(x, cookie_file))
-		else:
-			with open(cookie_file) as file_data:
-				cached_settings = json.load(file_data, object_hook=from_json)
-			# log_info_green('Using settings file: {0!s}'.format(cookie_file))
-
-			device_id = cached_settings.get('device_id')
-			# reuse auth cached_settings
-			api = Client(
-				username, password,
-				settings=cached_settings)
-
-	except (ClientCookieExpiredError) as e:
-		log_warn('The current cookie file for "{:s}" has expired, creating a new one...'.format(username))
-
-		# Login expired
-		# Do relogin but use default ua, keys and such
-		try:
-			api = Client(
-				username, password,
-				device_id=device_id,
-				on_login=lambda x: onlogin_callback(x, cookie_file))
-		except Exception as ee:
-			log_seperator()
-			log_error('An error occurred while trying to create a new cookie file: {:s}'.format(str(ee)))
-			if "getaddrinfo failed" in str(ee):
-				log_error('Could not resolve host, check your internet connection.')
-			elif "timed out" in str(ee):
-				log_error('The connection timed out, check your internet connection.')
-			elif "bad_password" in str(ee):
-				log_error('The password you entered is incorrect. Please try again.')
-			else:
-				log_error('{:s}'.format(ee.message))
-			log_seperator()
-			exit(1)
-
-	except ClientLoginError as e:
-		log_seperator()
-		log_error('Could not login: {:s}.'.format(json.loads(e.error_response).get("error_title", "Error title not available.")))
-		log_error('{:s}'.format(json.loads(e.error_response).get("message", "Not available")))
-		log_error('{:s}'.format(e.error_response))
-		log_seperator()
-		sys.exit(9)
-	except ClientError as e:
-		log_seperator()
-		try:
-			log_error('Unexpected exception: {:s}'.format(e.msg))
-			log_error('Message: {:s}'.format(json.loads(e.error_response).get("message", "Additional error information not available.")))
-			log_error('Code: {:d}'.format(e.code))
-			log_error('Full response:\n{:s}'.format(e.error_response))
-			log_whiteline()
-		except Exception as ee:
-			log_error('An error occurred while trying to handle a previous exception.')
-			log_error('1: {:s}'.format(str(e)))
-			log_error('2: {:s}'.format(str(ee)))
-			if "getaddrinfo failed" in str(ee):
-				log_error('Could not resolve host, check your internet connection.')
-			if "timed out" in str(ee):
-				log_error('The connection timed out, check your internet connection.')
-		log_seperator()
-		sys.exit(9)
-	except Exception as e:
-		if (str(e).startswith("unsupported pickle protocol")):
-			log_warn("This cookie file is not compatible with Python {}.".format(sys.version.split(' ')[0][0]))
-			log_warn("Please delete your cookie file '{}.json' and try again.".format(username))
-		else:
-			log_seperator()
-			log_error('Unexpected exception: {:s}'.format(e))
-		log_seperator()
-		sys.exit(99)
-	except KeyboardInterrupt as e:
-		log_seperator()
-		log_warn("The user authentication has been aborted.")
-		log_seperator()
-		sys.exit(0)
-
-	log_info_green('Successfully logged into user "{:s}".'.format(str(api.authenticated_user_name)))
-	if show_cookie_expiry.title() == 'True' and not force_use_login_args:
-		try:
-			cookie_expiry = api.cookie_jar.auth_expires
-			log_info_green('Cookie file expiry date: {:s}'.format(datetime.datetime.fromtimestamp(cookie_expiry).strftime('%Y-%m-%d at %I:%M:%S %p')))
-		except AttributeError as e:
-			log_warn('An error occurred while getting the cookie file expiry date: {:s}'.format(str(e)))
-
-	log_seperator()		
-	return api
+    cache_settings = api.settings
+    with open(cookie_file, 'w') as outfile:
+        json.dump(cache_settings, outfile, default=to_json)
+        logger.info('New cookie file was made: {0!s}'.format(cookie_file))
+        logger.separator()
+
+
+def authenticate(username, password, force_use_login_args=False):
+    ig_api = None
+    try:
+        if force_use_login_args:
+            logger.binfo("Overriding configuration file login with -u and -p arguments.")
+            logger.separator()
+        cookie_file = "{}.json".format(username)
+        if not os.path.isfile(cookie_file):
+            # settings file does not exist
+            logger.warn('Unable to find cookie file: {0!s}'.format(cookie_file))
+            logger.info('Creating a new one.')
+
+            # login new
+            ig_api = Client(
+                username, password,
+                on_login=lambda x: onlogin_callback(x, cookie_file))
+        else:
+            with open(cookie_file) as file_data:
+                cached_settings = json.load(file_data, object_hook=from_json)
+            # logger.info('Using settings file: {0!s}'.format(cookie_file))
+
+            device_id = cached_settings.get('device_id')
+            # reuse auth cached_settings
+            try:
+                ig_api = Client(
+                    username, password,
+                    settings=cached_settings)
+
+            except ClientCookieExpiredError as e:
+                logger.warn('The current cookie file has expired, creating a new one.')
+
+                ig_api = Client(
+                    username, password,
+                    device_id=device_id,
+                    on_login=lambda x: onlogin_callback(x, cookie_file))
+
+    except (ClientLoginError, ClientError) as e:
+        logger.separator()
+        logger.error('Could not login: {:s}'.format(
+            json.loads(e.error_response).get("error_title", "Error title not available.")))
+        logger.error('{:s}'.format(json.loads(e.error_response).get("message", "Not available")))
+        # logger.error('{:s}'.format(e.error_response))
+        logger.separator()
+    except Exception as e:
+        if str(e).startswith("unsupported pickle protocol"):
+            logger.warn("This cookie file is not compatible with Python {}.".format(sys.version.split(' ')[0][0]))
+            logger.warn("Please delete your cookie file '{}.json' and try again.".format(username))
+        else:
+            logger.separator()
+            logger.error('Unexpected exception: {:s}'.format(e))
+        logger.separator()
+    except KeyboardInterrupt:
+        logger.separator()
+        logger.warn("The user authentication has been aborted.")
+        logger.separator()
+
+    if ig_api:
+        logger.info('Successfully logged into account: {:s}'.format(str(ig_api.authenticated_user_name)))
+        if pil.show_cookie_expiry and not force_use_login_args:
+            try:
+                cookie_expiry = ig_api.cookie_jar.auth_expires
+                logger.info('Cookie file expiry date: {:s}'.format(
+                    datetime.datetime.fromtimestamp(cookie_expiry).strftime('%Y-%m-%d at %I:%M:%S %p')))
+            except AttributeError as e:
+                logger.warn('An error occurred while getting the cookie file expiry date: {:s}'.format(str(e)))
+
+        logger.separator()
+        return ig_api
+    else:
+        return None

+ 182 - 150
pyinstalive/comments.py

@@ -9,165 +9,197 @@ import os
 from socket import error as SocketError
 from socket import timeout
 from ssl import SSLError
+
 try:
-	# py2
-	from urllib2 import URLError
-	from httplib import HTTPException
+    # py2
+    from urllib2 import URLError
+    from httplib import HTTPException
 except ImportError:
-	# py3
-	from urllib.error import URLError
-	from http.client import HTTPException
+    # py3
+    from urllib.error import URLError
+    from http.client import HTTPException
+
 
-from .logger import log_seperator, supports_color, log_info_blue, log_info_green, log_warn, log_error, log_whiteline, log_plain
+try:
+    import logger
+    import helpers
+    import pil
+    import dlfuncs
+except ImportError:
+    from . import logger
+    from . import helpers
+    from . import pil
+    from . import dlfuncs
 
 from instagram_private_api import ClientError
 
 """
-This feature of PyInstaLive was originally written by https://github.com/taengstagram
-The code below and in downloader.py that's related to the comment downloading
-feature is modified by https://github.com/notcammy
+The content of this file was originally written by https://github.com/taengstagram
+The code has been edited for use in PyInstaLive.
 """
 
 
 class CommentsDownloader(object):
 
-	def __init__(self, api, broadcast, destination_file):
-		self.api = api
-		self.broadcast = broadcast
-		self.destination_file = destination_file
-		self.comments = []
-
-	def get_live(self, first_comment_created_at=0):
-		comments_collected = self.comments
-
-		before_count = len(comments_collected)
-		try:
-			comments_res = self.api.broadcast_comments(
-				self.broadcast.get('id'), last_comment_ts=first_comment_created_at)
-			comments = comments_res.get('comments', [])
-			first_comment_created_at = (
-				comments[0]['created_at_utc'] if comments else int(time.time() - 5))
-			comments_collected.extend(comments)
-			after_count = len(comments_collected)
-			if after_count > before_count:
-				broadcast = self.broadcast.copy()
-				broadcast.pop('segments', None)     # save space
-				broadcast['comments'] = comments_collected
-				with open(self.destination_file, 'w') as outfile:
-					json.dump(broadcast, outfile, indent=2)
-			self.comments = comments_collected
-
-		except (SSLError, timeout, URLError, HTTPException, SocketError) as e:
-			log_warn('Comment downloading error: %s' % e)
-		except ClientError as e:
-			if e.code == 500:
-				log_warn('Comment downloading ClientError: %d %s' % (e.code, e.error_response))
-			elif e.code == 400 and not e.msg:
-				log_warn('Comment downloading ClientError: %d %s' % (e.code, e.error_response))
-			else:
-				raise e
-		finally:
-			try:
-				time.sleep(4)
-			except KeyboardInterrupt:
-				return first_comment_created_at
-		return first_comment_created_at
-
-	def get_replay(self):
-		comments_collected = []
-		starting_offset = 0
-		encoding_tag = self.broadcast.get('encoding_tag')
-		while True:
-			try:
-				comments_res = self.api.replay_broadcast_comments(
-					self.broadcast.get('id'), starting_offset=starting_offset, encoding_tag=encoding_tag)
-				starting_offset = comments_res.get('ending_offset', 0)
-				comments = comments_res.get('comments', [])
-				comments_collected.extend(comments)
-				if not comments_res.get('comments') or not starting_offset:
-					break
-				time.sleep(4)
-			except:
-				pass
-
-		if comments_collected:
-			self.broadcast['comments'] = comments_collected
-			self.broadcast['initial_buffered_duration'] = 0
-			with open(self.destination_file, 'w') as outfile:
-				json.dump(self.broadcast, outfile, indent=2)
-		self.comments = comments_collected
-
-	def save(self):
-		broadcast = self.broadcast.copy()
-		broadcast.pop('segments', None)
-		broadcast['comments'] = self.comments
-		with open(self.destination_file, 'w') as outfile:
-			json.dump(broadcast, outfile, indent=2)
-
-	@staticmethod
-	def generate_log(comments, download_start_time, log_file, comments_delay=10.0):
-		comment_log_save_path = os.path.dirname(os.path.dirname(log_file))
-		comment_log_file_name = os.path.basename(log_file)
-		log_file = os.path.join(comment_log_save_path, comment_log_file_name)
-		python_version = sys.version.split(' ')[0]
-		subtitles_timeline = {}
-		wide_build = sys.maxunicode > 65536
-		for i, c in enumerate(comments):
-			if 'offset' in c:
-				for k in c.get('comment').keys():
-					c[k] = c.get('comment', {}).get(k)
-				c['created_at_utc'] = download_start_time + c.get('offset')
-			created_at_utc = str(2 * (c.get('created_at_utc') // 2))
-			comment_list = subtitles_timeline.get(created_at_utc) or []
-			comment_list.append(c)
-			subtitles_timeline[created_at_utc] = comment_list
-
-		if subtitles_timeline:
-			comment_errors = 0
-			total_comments = 0
-			timestamps = sorted(subtitles_timeline.keys())
-			mememe = False
-			subs = []
-			for i, tc in enumerate(timestamps):
-				t = subtitles_timeline[tc]
-				clip_start = int(tc) - int(download_start_time) + int(comments_delay)
-				if clip_start < 0:
-					clip_start = 0
-
-				comments_log = ''
-				for c in t:
-					try:
-						if python_version.startswith('3'):
-							if (c.get('user', {}).get('is_verified')):
-								comments_log+= '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{} {}: {}'.format(c.get('user', {}).get('username'), "(v)", c.get('text')))
-							else:
-								comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{}: {}'.format(c.get('user', {}).get('username'), c.get('text')))
-						else:
-							if not wide_build:
-								if (c.get('user', {}).get('is_verified')):
-									comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{} {}: {}'.format(c.get('user', {}).get('username'), "(v)", c.get('text').encode('ascii', 'ignore')))
-								else:
-									comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{}: {}'.format(c.get('user', {}).get('username'), c.get('text').encode('ascii', 'ignore')))
-							else:
-								if (c.get('user', {}).get('is_verified')):
-									comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{} {}: {}'.format(c.get('user', {}).get('username'), "(v)", c.get('text')))
-								else:
-									comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{}: {}'.format(c.get('user', {}).get('username'), c.get('text')))
-					except:
-						comment_errors += 1
-						try:
-							if (c.get('user', {}).get('is_verified')):
-								comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{} {}: {}'.format(c.get('user', {}).get('username'), "(v)", c.get('text').encode('ascii', 'ignore')))
-							else:
-								comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)), '{}: {}'.format(c.get('user', {}).get('username'), c.get('text').encode('ascii', 'ignore')))
-						except:
-							pass
-				total_comments += 1
-				subs.append(comments_log)
-				
-			with codecs.open(log_file, 'w', 'utf-8-sig') as log_outfile:
-				if python_version.startswith('2') and not wide_build:
-					log_outfile.write('This log was generated using Python {:s} without wide unicode support. This means characters such as emojis are not saved.\nUser comments without any text usually are comments that only had emojis.\nBuild Python 2 with the --enable-unicode=ucs4 argument or use Python 3 for full unicode support.\n\n'.format(python_version) + ''.join(subs))
-				else:
-					log_outfile.write(''.join(subs))
-			return comment_errors, total_comments
+    def __init__(self, destination_file):
+        self.api = pil.ig_api
+        self.broadcast = pil.livestream_obj
+        self.destination_file = destination_file
+        self.comments = []
+
+    def get_live(self, first_comment_created_at=0):
+        comments_collected = self.comments
+
+        before_count = len(comments_collected)
+        try:
+            comments_res = self.api.broadcast_comments(
+                self.broadcast.get('id'), last_comment_ts=first_comment_created_at)
+            comments = comments_res.get('comments', [])
+            first_comment_created_at = (
+                comments[0]['created_at_utc'] if comments else int(time.time() - 5))
+            comments_collected.extend(comments)
+            after_count = len(comments_collected)
+            if after_count > before_count:
+                broadcast = self.broadcast.copy()
+                broadcast.pop('segments', None)  # save space
+                broadcast['comments'] = comments_collected
+                with open(self.destination_file, 'w') as outfile:
+                    json.dump(broadcast, outfile, indent=2)
+            self.comments = comments_collected
+
+        except (SSLError, timeout, URLError, HTTPException, SocketError) as e:
+            logger.warn('Comment downloading error: %s' % e)
+        except ClientError as e:
+            if e.code == 500:
+                logger.warn('Comment downloading ClientError: %d %s' % (e.code, e.error_response))
+            elif e.code == 400 and not e.msg:
+                logger.warn('Comment downloading ClientError: %d %s' % (e.code, e.error_response))
+            else:
+                raise e
+        finally:
+            try:
+                time.sleep(4)
+            except KeyboardInterrupt:
+                return first_comment_created_at
+        return first_comment_created_at
+
+    def get_replay(self):
+        comments_collected = []
+        starting_offset = 0
+        encoding_tag = self.broadcast.get('encoding_tag')
+        while True:
+            try:
+                comments_res = self.api.replay_broadcast_comments(
+                    self.broadcast.get('id'), starting_offset=starting_offset, encoding_tag=encoding_tag)
+                starting_offset = comments_res.get('ending_offset', 0)
+                comments = comments_res.get('comments', [])
+                comments_collected.extend(comments)
+                if not comments_res.get('comments') or not starting_offset:
+                    break
+                time.sleep(4)
+            except Exception:
+                pass
+
+        if comments_collected:
+            self.broadcast['comments'] = comments_collected
+            self.broadcast['initial_buffered_duration'] = 0
+            with open(self.destination_file, 'w') as outfile:
+                json.dump(self.broadcast, outfile, indent=2)
+        self.comments = comments_collected
+
+    def save(self):
+        broadcast = self.broadcast.copy()
+        broadcast.pop('segments', None)
+        broadcast['comments'] = self.comments
+        with open(self.destination_file, 'w') as outfile:
+            json.dump(broadcast, outfile, indent=2)
+
+    @staticmethod
+    def generate_log(comments, download_start_time, log_file, comments_delay=10.0):
+        python_version = sys.version.split(' ')[0]
+        comments_timeline = {}
+        wide_build = sys.maxunicode > 65536
+        for i, c in enumerate(comments):
+            if 'offset' in c:
+                for k in c.get('comment').keys():
+                    c[k] = c.get('comment', {}).get(k)
+                c['created_at_utc'] = download_start_time + c.get('offset')
+            created_at_utc = str(2 * (c.get('created_at_utc') // 2))
+            comment_list = comments_timeline.get(created_at_utc) or []
+            comment_list.append(c)
+            comments_timeline[created_at_utc] = comment_list
+
+        if comments_timeline:
+            comment_errors = 0
+            total_comments = 0
+            timestamps = sorted(comments_timeline.keys())
+            subs = []
+            for i, tc in enumerate(timestamps):
+                t = comments_timeline[tc]
+                clip_start = int(tc) - int(download_start_time) + int(comments_delay)
+                if clip_start < 0:
+                    clip_start = 0
+
+                comments_log = ''
+                for c in t:
+                    try:
+                        if python_version.startswith('3'):
+                            if c.get('user', {}).get('is_verified'):
+                                comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                                                  '{} {}: {}'.format(c.get('user', {}).get('username'),
+                                                                                     "(v)", c.get('text')))
+                            else:
+                                comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                                                  '{}: {}'.format(c.get('user', {}).get('username'),
+                                                                                  c.get('text')))
+                        else:
+                            if not wide_build:
+                                if c.get('user', {}).get('is_verified'):
+                                    comments_log += '{}{}\n\n'.format(
+                                        time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                        '{} {}: {}'.format(c.get('user', {}).get('username'), "(v)",
+                                                           c.get('text').encode('ascii', 'ignore')))
+                                else:
+                                    comments_log += '{}{}\n\n'.format(
+                                        time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                        '{}: {}'.format(c.get('user', {}).get('username'),
+                                                        c.get('text').encode('ascii', 'ignore')))
+                            else:
+                                if c.get('user', {}).get('is_verified'):
+                                    comments_log += '{}{}\n\n'.format(
+                                        time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                        '{} {}: {}'.format(c.get('user', {}).get('username'), "(v)", c.get('text')))
+                                else:
+                                    comments_log += '{}{}\n\n'.format(
+                                        time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                        '{}: {}'.format(c.get('user', {}).get('username'), c.get('text')))
+                    except Exception:
+                        comment_errors += 1
+                        try:
+                            if c.get('user', {}).get('is_verified'):
+                                comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                                                  '{} {}: {}'.format(c.get('user', {}).get('username'),
+                                                                                     "(v)",
+                                                                                     c.get('text').encode('ascii',
+                                                                                                          'ignore')))
+                            else:
+                                comments_log += '{}{}\n\n'.format(time.strftime('%H:%M:%S\n', time.gmtime(clip_start)),
+                                                                  '{}: {}'.format(c.get('user', {}).get('username'),
+                                                                                  c.get('text').encode('ascii',
+                                                                                                       'ignore')))
+                        except Exception:
+                            pass
+                total_comments += 1
+                subs.append(comments_log)
+
+            with codecs.open(log_file, 'w', 'utf-8-sig') as log_outfile:
+                if python_version.startswith('2') and not wide_build:
+                    log_outfile.write(
+                        'This log was generated using Python {:s} without wide unicode support. This means characters '
+                        'such as emotes are not saved.\nUser comments without any text usually are comments that only '
+                        'had emotes.\nBuild Python 2 with the --enable-unicode=ucs4 argument or use Python 3 for full '
+                        'unicode support.\n\n'.format(
+                            python_version) + ''.join(subs))
+                else:
+                    log_outfile.write(''.join(subs))
+            return comment_errors, total_comments

+ 22 - 0
pyinstalive/constants.py

@@ -0,0 +1,22 @@
+import sys
+
+
+class Constants:
+    SCRIPT_VER = "3.0.0"
+    PYTHON_VER = sys.version.split(' ')[0]
+    CONFIG_TEMPLATE = """
+[pyinstalive]
+username = johndoe
+password = grapefruits
+download_path = {:s}
+download_lives = True
+download_replays = True
+download_comments = true
+show_cookie_expiry = True
+log_to_file = True
+ffmpeg_path = 
+run_at_start =
+run_at_finish =
+use_locks = True
+clear_temp_files = False
+    """

+ 490 - 0
pyinstalive/dlfuncs.py

@@ -0,0 +1,490 @@
+import os
+import shutil
+import json
+import threading
+import time
+
+from xml.dom.minidom import parseString
+
+from instagram_private_api import ClientConnectionError
+from instagram_private_api import ClientError
+from instagram_private_api import ClientThrottledError
+from instagram_private_api_extensions import live
+from instagram_private_api_extensions import replay
+
+try:
+    import logger
+    import helpers
+    import pil
+    import dlfuncs
+    import assembler
+    from constants import Constants
+    from comments import CommentsDownloader
+except ImportError:
+    from . import logger
+    from . import helpers
+    from . import pil
+    from . import assembler
+    from . import dlfuncs
+    from .constants import Constants
+    from .comments import CommentsDownloader
+
+
+def get_stream_duration(duration_type):
+    try:
+        # For some reason the published_time is roughly 40 seconds behind real world time
+        if duration_type == 0: # Airtime duration
+            stream_started_mins, stream_started_secs = divmod((int(time.time()) - pil.livestream_obj.get("published_time") + 40), 60)
+        if duration_type == 1: # Download duration
+            stream_started_mins, stream_started_secs = divmod((int(time.time()) - int(pil.epochtime)), 60)
+        if duration_type == 2: # Missing duration
+            if (int(pil.epochtime) - pil.livestream_obj.get("published_time") + 40) <= 0:
+                stream_started_mins, stream_started_secs = 0, 0 # Download started 'earlier' than actual broadcast, assume started at the same time instead
+            else:
+                stream_started_mins, stream_started_secs = divmod((int(pil.epochtime) - pil.livestream_obj.get("published_time") + 40), 60)
+
+        if stream_started_mins < 0:
+            stream_started_mins = 0
+        if stream_started_secs < 0:
+            stream_started_secs = 0
+        stream_duration_str = '%d minutes' % stream_started_mins
+        if stream_started_secs:
+            stream_duration_str += ' and %d seconds' % stream_started_secs
+        return stream_duration_str
+    except Exception as e:
+        return "Not available"
+
+
+def get_user_id():
+    is_user_id = False
+    user_id = None
+    try:
+        user_id = int(pil.dl_user)
+        is_user_id = True
+    except ValueError:
+        try:
+            user_res = pil.ig_api.username_info(pil.dl_user)
+            user_id = user_res.get('user', {}).get('pk')
+        except ClientConnectionError as cce:
+            logger.error(
+                "Could not get user info for '{:s}': {:d} {:s}".format(pil.dl_user, cce.code, str(cce)))
+            if "getaddrinfo failed" in str(cce):
+                logger.error('Could not resolve host, check your internet connection.')
+            if "timed out" in str(cce):
+                logger.error('The connection timed out, check your internet connection.')
+        except ClientThrottledError as cte:
+            logger.error(
+                "Could not get user info for '{:s}': {:d} {:s}".format(pil.dl_user, cte.code, str(cte)))
+        except ClientError as ce:
+            logger.error(
+                "Could not get user info for '{:s}': {:d} {:s}".format(pil.dl_user, ce.code, str(ce)))
+            if "Not Found" in str(ce):
+                logger.error('The specified user does not exist.')
+        except Exception as e:
+            logger.error("Could not get user info for '{:s}': {:s}".format(pil.dl_user, str(e)))
+        except KeyboardInterrupt:
+            logger.binfo("Aborted getting user info for '{:s}', exiting.".format(pil.dl_user))
+    if user_id and is_user_id:
+        logger.info("Getting info for '{:s}' successful. Assuming input is an user Id.".format(pil.dl_user))
+        logger.separator()
+        return user_id
+    elif user_id:
+        logger.info("Getting info for '{:s}' successful.".format(pil.dl_user))
+        logger.separator()
+        return user_id
+    else:
+        return None
+
+
+def get_broadcasts_info():
+    try:
+        user_id = get_user_id()
+        if user_id:
+            broadcasts = pil.ig_api.user_story_feed(user_id)
+            pil.livestream_obj = broadcasts.get('broadcast')
+            pil.replays_obj = broadcasts.get('post_live_item', {}).get('broadcasts', [])
+            return True
+        else:
+            return False
+    except Exception as e:
+        logger.error('Could not finish checking: {:s}'.format(str(e)))
+        if "timed out" in str(e):
+            logger.error('The connection timed out, check your internet connection.')
+        logger.separator()
+        return False
+    except KeyboardInterrupt:
+        logger.binfo('Aborted checking for livestreams and replays, exiting.'.format(pil.dl_user))
+        logger.separator()
+        return False
+    except ClientThrottledError as cte:
+        logger.error('Could not check because you are making too many requests at this time.')
+        logger.separator()
+        return False
+
+
+def merge_segments():
+    try:
+        if pil.run_at_finish:
+            try:
+                thread = threading.Thread(target=helpers.run_command, args=(pil.run_at_finish,))
+                thread.daemon = True
+                thread.start()
+                logger.binfo("Launched finish command: {:s}".format(pil.run_at_finish))
+            except Exception as e:
+                logger.warn('Could not execute command: {:s}'.format(str(e)))
+
+        live_mp4_file = '{}{}_{}_{}_live.mp4'.format(pil.dl_path, pil.datetime_compat, pil.dl_user,
+                                                     pil.livestream_obj.get('id'))
+
+        live_segments_path = os.path.normpath(pil.broadcast_downloader.output_dir)
+
+        if pil.segments_json_thread_worker and pil.segments_json_thread_worker.is_alive():
+            pil.segments_json_thread_worker.join()
+
+        if pil.comment_thread_worker and pil.comment_thread_worker.is_alive():
+            logger.info("Waiting for comment downloader to finish.")
+            pil.comment_thread_worker.join()
+        logger.info('Merging downloaded files into video.')
+        try:
+            pil.broadcast_downloader.stitch(live_mp4_file, cleartempfiles=pil.clear_temp_files)
+            logger.info('Successfully merged downloaded files into video.')
+            helpers.remove_lock()
+        except ValueError as e:
+            logger.separator()
+            logger.error('Could not merge downloaded files: {:s}'.format(str(e)))
+            if os.listdir(live_segments_path):
+                logger.separator()
+                logger.binfo("Segment directory is not empty. Trying to merge again.")
+                logger.separator()
+                pil.assemble_arg = live_mp4_file.replace(".mp4", "_downloads.json")
+                assembler.assemble(user_called=False)
+            else:
+                logger.separator()
+                logger.error("Segment directory is empty. There is nothing to merge.")
+                logger.separator()
+            helpers.remove_lock()
+        except Exception as e:
+            logger.error('Could not merge downloaded files: {:s}'.format(str(e)))
+            helpers.remove_lock()
+    except KeyboardInterrupt:
+        logger.binfo('Aborted merging process, no video was created.')
+        helpers.remove_lock()
+
+
+def download_livestream():
+    try:
+        def print_status(sep=True):
+            heartbeat_info = pil.ig_api.broadcast_heartbeat_and_viewercount(pil.livestream_obj.get('id'))
+            viewers = pil.livestream_obj.get('viewer_count', 0)
+            if sep:
+                logger.separator()
+            else:
+                logger.info('Username    : {:s}'.format(pil.dl_user))
+            logger.info('Viewers     : {:s} watching'.format(str(int(viewers))))
+            logger.info('Airing time : {:s}'.format(get_stream_duration(0)))
+            logger.info('Status      : {:s}'.format(heartbeat_info.get('broadcast_status').title()))
+            return heartbeat_info.get('broadcast_status') not in ['active', 'interrupted']
+
+        mpd_url = (pil.livestream_obj.get('dash_manifest')
+                   or pil.livestream_obj.get('dash_abr_playback_url')
+                   or pil.livestream_obj.get('dash_playback_url'))
+
+        pil.live_folder_path = '{}{}_{}_{}_live_downloads'.format(pil.dl_path, pil.datetime_compat,
+                                                                  pil.dl_user, pil.livestream_obj.get('id'))
+        pil.broadcast_downloader = live.Downloader(
+            mpd=mpd_url,
+            output_dir=pil.live_folder_path,
+            user_agent=pil.ig_api.user_agent,
+            max_connection_error_retry=3,
+            duplicate_etag_retry=30,
+            callback_check=print_status,
+            mpd_download_timeout=3,
+            download_timeout=3,
+            ffmpeg_binary=pil.ffmpeg_path)
+    except Exception as e:
+        logger.error('Could not start downloading livestream: {:s}'.format(str(e)))
+        logger.separator()
+        helpers.remove_lock()
+    try:
+        broadcast_owner = pil.livestream_obj.get('broadcast_owner', {}).get('username')
+        try:
+            broadcast_guest = pil.livestream_obj.get('cobroadcasters', {})[0].get('username')
+        except Exception:
+            broadcast_guest = None
+        if broadcast_owner != pil.dl_user:
+            logger.binfo('This livestream is a dual-live, the owner is "{}".'.format(broadcast_owner))
+            broadcast_guest = None
+        if broadcast_guest:
+            logger.binfo('This livestream is a dual-live, the current guest is "{}".'.format(broadcast_guest))
+            pil.has_guest = broadcast_guest
+        logger.separator()
+        print_status(False)
+        logger.separator()
+        helpers.create_lock_folder()
+        pil.segments_json_thread_worker = threading.Thread(target=helpers.generate_json_segments)
+        pil.segments_json_thread_worker.start()
+        logger.info('Downloading livestream, press [CTRL+C] to abort.')
+
+        if pil.run_at_start:
+            try:
+                thread = threading.Thread(target=helpers.run_command, args=(pil.run_at_start,))
+                thread.daemon = True
+                thread.start()
+                logger.binfo("Launched start command: {:s}".format(pil.run_at_start))
+            except Exception as e:
+                logger.warn('Could not launch command: {:s}'.format(str(e)))
+
+        if pil.dl_comments:
+            try:
+                comments_json_file = '{}{}_{}_{}_live_comments.json'.format(pil.dl_path, pil.datetime_compat,
+                                                                            pil.dl_user, pil.livestream_obj.get('id'))
+                pil.comment_thread_worker = threading.Thread(target=get_live_comments, args=(comments_json_file,))
+                pil.comment_thread_worker.start()
+            except Exception as e:
+                logger.error('An error occurred while downloading comments: {:s}'.format(str(e)))
+        pil.broadcast_downloader.run()
+        logger.separator()
+        logger.info("The livestream has been ended by the user.")
+        logger.separator()
+        logger.info('Airtime duration  : {}'.format(get_stream_duration(0)))
+        logger.info('Download duration : {}'.format(get_stream_duration(1)))
+        logger.info('Missing (approx.) : {}'.format(get_stream_duration(2)))
+        logger.separator()
+        merge_segments()
+    except KeyboardInterrupt:
+        logger.separator()
+        logger.binfo('The download has been aborted.')
+        logger.separator()
+        logger.info('Airtime duration  : {}'.format(get_stream_duration(0)))
+        logger.info('Download duration : {}'.format(get_stream_duration(1)))
+        logger.info('Missing (approx.) : {}'.format(get_stream_duration(2)))
+        logger.separator()
+        if not pil.broadcast_downloader.is_aborted:
+            pil.broadcast_downloader.stop()
+            merge_segments()
+
+
+def download_replays():
+    try:
+        try:
+            logger.info('Amount of replays    : {:s}'.format(str(len(pil.replays_obj))))
+            for replay_index, replay_obj in enumerate(pil.replays_obj):
+                bc_dash_manifest = parseString(replay_obj.get('dash_manifest')).getElementsByTagName('Period')
+                bc_duration_raw = bc_dash_manifest[0].getAttribute("duration")
+                bc_minutes = (bc_duration_raw.split("H"))[1].split("M")[0]
+                bc_seconds = ((bc_duration_raw.split("M"))[1].split("S")[0]).split('.')[0]
+                logger.info(
+                    'Replay {:s} duration    : {:s} minutes and {:s} seconds'.format(str(replay_index + 1), bc_minutes,
+                                                                                     bc_seconds))
+        except Exception as e:
+            logger.warn("An error occurred while getting replay duration information: {:s}".format(str(e)))
+        logger.separator()
+        logger.info("Downloading replays, press [CTRL+C] to abort.")
+        logger.separator()
+        for replay_index, replay_obj in enumerate(pil.replays_obj):
+            exists = False
+            pil.livestream_obj = replay_obj
+            if Constants.PYTHON_VER[0][0] == '2':
+                directories = (os.walk(pil.dl_path).next()[1])
+            else:
+                directories = (os.walk(pil.dl_path).__next__()[1])
+
+            for directory in directories:
+                if (str(replay_obj.get('id')) in directory) and ("_live_" not in directory):
+                    logger.binfo("Already downloaded a replay with ID '{:s}'.".format(str(replay_obj.get('id'))))
+                    exists = True
+            if not exists:
+                current = replay_index + 1
+                logger.info(
+                    "Downloading replay {:s} of {:s} with ID '{:s}'.".format(str(current), str(len(pil.replays_obj)),
+                                                                               str(replay_obj.get('id'))))
+                pil.live_folder_path = '{}{}_{}_{}_replay_downloads'.format(pil.dl_path, pil.datetime_compat,
+                                                                            pil.dl_user, pil.livestream_obj.get('id'))
+                broadcast_downloader = replay.Downloader(
+                    mpd=replay_obj.get('dash_manifest'),
+                    output_dir=pil.live_folder_path,
+                    user_agent=pil.ig_api.user_agent,
+                    ffmpeg_binary=pil.ffmpeg_path)
+                if pil.use_locks:
+                    helpers.create_lock_folder()
+                replay_mp4_file = '{}{}_{}_{}_replay.mp4'.format(pil.dl_path, pil.datetime_compat,
+                                                                 pil.dl_user, pil.livestream_obj.get('id'))
+
+                comments_json_file = '{}{}_{}_{}_live_comments.json'.format(pil.dl_path, pil.datetime_compat,
+                                                                            pil.dl_user, pil.livestream_obj.get('id'))
+
+                pil.comment_thread_worker = threading.Thread(target=get_replay_comments, args=(comments_json_file,))
+
+                broadcast_downloader.download(replay_mp4_file, cleartempfiles=pil.clear_temp_files)
+
+                if pil.dl_comments:
+                    logger.info("Downloading replay comments.")
+                    try:
+                        get_replay_comments(comments_json_file)
+                    except Exception as e:
+                        logger.error('An error occurred while downloading comments: {:s}'.format(str(e)))
+
+                logger.info("Finished downloading replay {:s} of {:s}.".format(str(current), str(len(pil.replays_obj))))
+                try:
+                    os.remove(os.path.join(pil.live_folder_path, 'folder.lock'))
+                except Exception:
+                    pass
+
+                if current != len(pil.replays_obj):
+                    logger.separator()
+
+        logger.separator()
+        logger.info("Finished downloading all available replays.")
+        logger.separator()
+        helpers.remove_lock()
+    except Exception as e:
+        logger.error('Could not save replay: {:s}'.format(str(e)))
+        logger.separator()
+        helpers.remove_lock()
+    except KeyboardInterrupt:
+        logger.separator()
+        logger.binfo('The download has been aborted by the user, exiting.')
+        logger.separator()
+        try:
+            shutil.rmtree(pil.live_folder_path)
+        except Exception as e:
+            logger.error("Could not remove segment folder: {:s}".format(str(e)))
+        helpers.remove_lock()
+
+
+def download_following():
+    try:
+        logger.info("Checking following users for any livestreams or replays.")
+        broadcast_f_list = pil.ig_api.reels_tray()
+        usernames_available = []
+        if broadcast_f_list['broadcasts']:
+            for broadcast_f in broadcast_f_list['broadcasts']:
+                username = broadcast_f['broadcast_owner']['username']
+                if username not in usernames_available:
+                    usernames_available.append(username)
+
+        if broadcast_f_list.get('post_live', {}).get('post_live_items', []):
+            for broadcast_r in broadcast_f_list.get('post_live', {}).get('post_live_items', []):
+                for broadcast_f in broadcast_r.get("broadcasts", []):
+                    username = broadcast_f['broadcast_owner']['username']
+                    if username not in usernames_available:
+                        usernames_available.append(username)
+        logger.separator()
+        if usernames_available:
+            logger.info("The following users have available livestreams or replays:")
+            logger.info(', '.join(usernames_available))
+            logger.separator()
+            for user in usernames_available:
+                try:
+                    if os.path.isfile(os.path.join(pil.dl_path, user + '.lock')):
+                        logger.warn("Lock file is already present for '{:s}', there is probably another download "
+                                    "ongoing!".format(user))
+                        logger.warn("If this is not the case, manually delete the file '{:s}' and try again.".format(user + '.lock'))
+                    else:
+                        logger.info("Launching daemon process for '{:s}'.".format(user))
+                        start_result = helpers.run_command("pyinstalive -d {:s} -cp '{:s}' -dp '{:s}'".format(user, pil.config_path, pil.dl_path))
+                        if start_result:
+                            logger.warn("Could not start processs: {:s}".format(str(start_result)))
+                        else:
+                            logger.info("Process started successfully.")
+                    logger.separator()
+                    time.sleep(2)
+                except Exception as e:
+                    logger.warn("Could not start processs: {:s}".format(str(e)))
+                except KeyboardInterrupt:
+                    logger.binfo('The process launching has been aborted by the user.')
+                    logger.separator()
+        else:
+            logger.info("There are currently no available livestreams or replays.")
+            logger.separator()
+    except Exception as e:
+        logger.error("Could not finish checking following users: {:s}".format(str(e)))
+    except KeyboardInterrupt:
+        logger.separator()
+        logger.binfo('The checking process has been aborted by the user.')
+        logger.separator()
+
+
+def get_live_comments(comments_json_file):
+    try:
+        comments_downloader = CommentsDownloader(destination_file=comments_json_file)
+        first_comment_created_at = 0
+
+        try:
+            while not pil.broadcast_downloader.is_aborted:
+                if 'initial_buffered_duration' not in pil.livestream_obj and pil.broadcast_downloader.initial_buffered_duration:
+                    pil.livestream_obj['initial_buffered_duration'] = pil.broadcast_downloader.initial_buffered_duration
+                    comments_downloader.broadcast = pil.livestream_obj
+                first_comment_created_at = comments_downloader.get_live(first_comment_created_at)
+        except ClientError as e:
+            if not 'media has been deleted' in e.error_response:
+                logger.warn("Comment collection ClientError: %d %s" % (e.code, e.error_response))
+
+        try:
+            if comments_downloader.comments:
+                comments_downloader.save()
+                comments_log_file = comments_json_file.replace('.json', '.log')
+                comment_errors, total_comments = CommentsDownloader.generate_log(
+                    comments_downloader.comments, pil.epochtime, comments_log_file,
+                    comments_delay=pil.broadcast_downloader.initial_buffered_duration)
+                if len(comments_downloader.comments) == 1:
+                    logger.info("Successfully saved 1 comment.")
+                    os.remove(comments_json_file)
+                    logger.separator()
+                    return True
+                else:
+                    if comment_errors:
+                        logger.warn(
+                            "Successfully saved {:s} comments but {:s} comments are (partially) missing.".format(
+                                str(total_comments), str(comment_errors)))
+                    else:
+                        logger.info("Successfully saved {:s} comments.".format(str(total_comments)))
+                    os.remove(comments_json_file)
+                    logger.separator()
+                    return True
+            else:
+                logger.info("There are no available comments to save.")
+                logger.separator()
+                return False
+        except Exception as e:
+            logger.error('Could not save comments: {:s}'.format(str(e)))
+            return False
+    except KeyboardInterrupt as e:
+        logger.binfo("Downloading livestream comments has been aborted.")
+        return False
+
+
+def get_replay_comments(comments_json_file):
+    try:
+        comments_downloader = CommentsDownloader(destination_file=comments_json_file)
+        comments_downloader.get_replay()
+        try:
+            if comments_downloader.comments:
+                comments_log_file = comments_json_file.replace('.json', '.log')
+                comment_errors, total_comments = CommentsDownloader.generate_log(
+                    comments_downloader.comments, pil.livestream_obj.get('published_time'), comments_log_file,
+                    comments_delay=0)
+                if total_comments == 1:
+                    logger.info("Successfully saved 1 comment to logfile.")
+                    os.remove(comments_json_file)
+                    logger.separator()
+                    return True
+                else:
+                    if comment_errors:
+                        logger.warn(
+                            "Successfully saved {:s} comments but {:s} comments are (partially) missing.".format(
+                                str(total_comments), str(comment_errors)))
+                    else:
+                        logger.info("Successfully saved {:s} comments.".format(str(total_comments)))
+                    os.remove(comments_json_file)
+                    logger.separator()
+                    return True
+            else:
+                logger.info("There are no available comments to save.")
+                return False
+        except Exception as e:
+            logger.error('Could not save comments to logfile: {:s}'.format(str(e)))
+            return False
+    except KeyboardInterrupt as e:
+        logger.binfo("Downloading replay comments has been aborted.")
+        return False

+ 55 - 647
pyinstalive/downloader.py

@@ -1,647 +1,55 @@
-import os
-import shutil
-import subprocess
-import sys
-import threading
-import time
-import shlex
-import json
-
-from xml.dom.minidom import parse, parseString
-
-from instagram_private_api import ClientConnectionError
-from instagram_private_api import ClientError
-from instagram_private_api import ClientThrottledError
-from instagram_private_api_extensions import live
-from instagram_private_api_extensions import replay
-
-from .comments import CommentsDownloader
-from .logger import log_seperator, supports_color, log_info_blue, log_info_green, log_warn, log_error, log_whiteline, log_plain
-
-
-
-def start_single(instagram_api_arg, download_arg, settings_arg):
-	global instagram_api
-	global user_to_download
-	global broadcast
-	global settings
-	settings = settings_arg
-	instagram_api = instagram_api_arg
-	user_to_download = download_arg
-	try:
-		if not os.path.isfile(os.path.join(settings.save_path, user_to_download + '.lock')):
-			if settings.use_locks.title() == "True":
-				open(os.path.join(settings.save_path, user_to_download + '.lock'), 'a').close()
-		else:
-			log_warn("Lock file is already present for this user, there is probably another download ongoing!")
-			log_warn("If this is not the case, manually delete the file '{:s}' and try again.".format(user_to_download + '.lock'))
-			log_seperator()
-			sys.exit(1)
-	except Exception:
-		log_warn("Lock file could not be created. Downloads started from -df might cause problems!")
-	get_user_info(user_to_download)
-
-def start_multiple(instagram_api_arg, settings_arg, proc_arg, initialargs_arg):
-	try:
-		log_info_green("Checking following users for any livestreams or replays...")
-		broadcast_f_list = instagram_api_arg.reels_tray()
-		usernames_available = []
-		if broadcast_f_list['broadcasts']:
-			for broadcast_f in broadcast_f_list['broadcasts']:
-				username = broadcast_f['broadcast_owner']['username']
-				if username not in usernames_available:
-					usernames_available.append(username)
-
-		if broadcast_f_list.get('post_live', {}).get('post_live_items', []):
-			for broadcast_r in broadcast_f_list.get('post_live', {}).get('post_live_items', []):
-				for broadcast_f in broadcast_r.get("broadcasts", []):
-					username = broadcast_f['broadcast_owner']['username']
-					if username not in usernames_available:
-						usernames_available.append(username)
-		log_seperator()
-		if usernames_available:
-			log_info_green("The following users have available livestreams or replays:")
-			log_info_green(', '.join(usernames_available))
-			log_seperator()
-			for user in usernames_available:
-				try:
-					if os.path.isfile(os.path.join(settings_arg.save_path, user + '.lock')):
-						log_warn("Lock file is already present for '{:s}', there is probably another download ongoing!".format(user))
-						log_warn("If this is not the case, manually delete the file '{:s}' and try again.".format(user + '.lock'))
-					else:
-						log_info_green("Launching daemon process for '{:s}'...".format(user))
-						if not initialargs_arg.savepath and not initialargs_arg.configpath:
-							start_result = run_command("{:s} -d {:s}".format(proc_arg, user))
-						elif initialargs_arg.savepath and initialargs_arg.configpath:
-							start_result = run_command("{:s} -d {:s} -cp '{:s}' -sp '{:s}'".format(proc_arg, user, settings_arg.custom_config_path, settings_arg.save_path))
-						elif initialargs_arg.savepath:
-							start_result = run_command("{:s} -d {:s} -sp '{:s}'".format(proc_arg, user, settings_arg.save_path))
-						elif initialargs_arg.configpath:
-							start_result = run_command("{:s} -d {:s} -cp '{:s}'".format(proc_arg, user, settings_arg.custom_config_path))
-						if start_result:
-							log_info_green("Could not start processs: {:s}".format(str(start_result)))
-						else:
-							log_info_green("Process started successfully.")
-					log_seperator()	
-					time.sleep(2)
-				except Exception as e:
-					log_error("Could not start processs: {:s}".format(str(e)))
-				except KeyboardInterrupt:
-					log_info_blue('The process launching has been aborted by the user.')
-					log_seperator()				
-					sys.exit(0)
-		else:
-			log_info_green("There are currently no available livestreams or replays.")
-			log_seperator()
-			sys.exit(0)
-	except Exception as e:
-		log_error("Could not finish checking following users: {:s}".format(str(e)))
-		sys.exit(1)
-	except KeyboardInterrupt:
-		log_seperator()
-		log_info_blue('The checking process has been aborted by the user.')
-		log_seperator()
-		sys.exit(0)
-
-
-
-def run_command(command):
-	try:
-		FNULL = open(os.devnull, 'w')
-		subprocess.Popen(shlex.split(command), stdout=FNULL, stderr=subprocess.STDOUT)
-		return False
-	except Exception as e:
-		return str(e)
-
-
-
-def get_stream_duration(compare_time, broadcast=None):
-	try:
-		had_wrong_time = False
-		if broadcast:
-			if (int(time.time()) < int(compare_time)):
-				had_wrong_time = True				
-				corrected_compare_time = int(compare_time) - 5
-				download_time = int(time.time()) - int(corrected_compare_time)
-			else:
-				download_time = int(time.time()) - int(compare_time)
-			stream_time = int(time.time()) - int(broadcast.get('published_time'))
-			stream_started_mins, stream_started_secs = divmod(stream_time - download_time, 60)
-		else:
-			if (int(time.time()) < int(compare_time)):	
-				had_wrong_time = True			
-				corrected_compare_time = int(compare_time) - 5
-				stream_started_mins, stream_started_secs = divmod((int(time.time()) - int(corrected_compare_time)), 60)
-			else:
-				stream_started_mins, stream_started_secs = divmod((int(time.time()) - int(compare_time)), 60)
-		stream_duration_str = '%d minutes' % stream_started_mins
-		if stream_started_secs:
-			stream_duration_str += ' and %d seconds' % stream_started_secs
-		if had_wrong_time:
-			return "{:s} (corrected)".format(stream_duration_str)
-		else:
-			return stream_duration_str
-	except Exception as e:
-		return "Not available"
-
-
-
-def download_livestream(broadcast):
-	try:
-		def print_status(sep=True):
-			heartbeat_info = instagram_api.broadcast_heartbeat_and_viewercount(broadcast.get('id'))
-			viewers = broadcast.get('viewer_count', 0)
-			if sep:
-				log_seperator()
-			log_info_green('Viewers     : {:s} watching'.format(str(int(viewers))))
-			log_info_green('Airing time : {:s}'.format(get_stream_duration(broadcast.get('published_time'))))
-			log_info_green('Status      : {:s}'.format(heartbeat_info.get('broadcast_status').title()))
-			return heartbeat_info.get('broadcast_status') not in ['active', 'interrupted']
-
-		mpd_url = (broadcast.get('dash_manifest')
-				 or broadcast.get('dash_abr_playback_url')
-				 or broadcast.get('dash_playback_url'))
-
-		output_dir = '{}{}_{}_{}_{}_live_downloads'.format(settings.save_path, settings.current_date, user_to_download, broadcast.get('id'), settings.current_time)
-
-		broadcast_downloader = live.Downloader(
-			mpd=mpd_url,
-			output_dir=output_dir,
-			user_agent=instagram_api.user_agent,
-			max_connection_error_retry=3,
-			duplicate_etag_retry=30,
-			callback_check=print_status,
-			mpd_download_timeout=3,
-			download_timeout=3,
-			ffmpeg_binary=settings.ffmpeg_path)
-	except Exception as e:
-		log_error('Could not start downloading livestream: {:s}'.format(str(e)))
-		log_seperator()
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-		sys.exit(1)
-	try:
-		log_info_green('Livestream found, beginning download...')
-		broadcast_owner = broadcast.get('broadcast_owner', {}).get('username')
-		try:
-			broadcast_guest = broadcast.get('cobroadcasters', {})[0].get('username')
-		except Exception:
-			broadcast_guest = None
-		if (broadcast_owner != user_to_download):
-			log_info_blue('This livestream is a dual-live, the owner is "{}".'.format(broadcast_owner))
-			broadcast_guest = None
-		if broadcast_guest:
-			log_info_blue('This livestream is a dual-live, the current guest is "{}".'.format(broadcast_guest))
-		log_seperator()
-		log_info_green('Username    : {:s}'.format(user_to_download))
-		print_status(False)
-		log_info_green('MPD URL     : {:s}'.format(mpd_url))
-		log_seperator()
-		if settings.use_locks.title() == "True":
-			open(os.path.join(output_dir, 'folder.lock'), 'a').close()
-		log_info_green('Downloading livestream... press [CTRL+C] to abort.')
-
-		if (settings.run_at_start is not "None"):
-			try:
-				thread = threading.Thread(target=run_command, args=(settings.run_at_start,))
-				thread.daemon = True
-				thread.start()
-				log_info_green("Command executed: \033[94m{:s}".format(settings.run_at_start))
-			except Exception as e:
-				log_warn('Could not execute command: {:s}'.format(str(e)))
-
-
-		comment_thread_worker = None
-		if settings.save_comments.title() == "True":
-			try:
-				comments_json_file = os.path.join(output_dir, '{}_{}_{}_{}_live_comments.json'.format(settings.current_date, user_to_download, broadcast.get('id'), settings.current_time))
-				comment_thread_worker = threading.Thread(target=get_live_comments, args=(instagram_api, broadcast, comments_json_file, broadcast_downloader,))
-				comment_thread_worker.start()
-			except Exception as e:
-				log_error('An error occurred while downloading comments: {:s}'.format(str(e)))
-		broadcast_downloader.run()
-		log_seperator()
-		log_info_green('Download duration : {}'.format(get_stream_duration(int(settings.current_time))))
-		log_info_green('Stream duration   : {}'.format(get_stream_duration(broadcast.get('published_time'))))
-		log_info_green('Missing (approx.) : {}'.format(get_stream_duration(int(settings.current_time), broadcast)))
-		log_seperator()
-		stitch_video(broadcast_downloader, broadcast, comment_thread_worker)
-	except KeyboardInterrupt:
-		log_seperator()
-		log_info_blue('The download has been aborted by the user.')
-		log_seperator()
-		log_info_green('Download duration : {}'.format(get_stream_duration(int(settings.current_time))))
-		log_info_green('Stream duration   : {}'.format(get_stream_duration(broadcast.get('published_time'))))
-		log_info_green('Missing (approx.) : {}'.format(get_stream_duration(int(settings.current_time), broadcast)))
-		log_seperator()
-		if not broadcast_downloader.is_aborted:
-			broadcast_downloader.stop()
-			stitch_video(broadcast_downloader, broadcast, comment_thread_worker)
-	except Exception as e:
-		log_error("Could not download livestream: {:s}".format(str(e)))
-		try:
-			os.remove(os.path.join(output_dir, 'folder.lock'))
-		except Exception:
-			pass
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-
-
-def stitch_video(broadcast_downloader, broadcast, comment_thread_worker):
-	try:
-		live_mp4_file = '{}{}_{}_{}_{}_live.mp4'.format(settings.save_path, settings.current_date, user_to_download, broadcast.get('id'), settings.current_time)
-		live_folder_path = "{:s}_downloads".format(live_mp4_file.split('.mp4')[0])
-
-		if comment_thread_worker and comment_thread_worker.is_alive():
-			log_info_green("Waiting for comment downloader to end cycle...")
-			comment_thread_worker.join()
-
-		if (settings.run_at_finish is not "None"):
-			try:
-				thread = threading.Thread(target=run_command, args=(settings.run_at_finish,))
-				thread.daemon = True
-				thread.start()
-				log_info_green("Command executed: \033[94m{:s}".format(settings.run_at_finish))
-			except Exception as e:
-				log_warn('Could not execute command: {:s}'.format(str(e)))
-
-		log_info_green('Stitching downloaded files into video...')		
-		try:
-			if settings.clear_temp_files.title() == "True":
-				broadcast_downloader.stitch(live_mp4_file, cleartempfiles=True)
-			else:
-				broadcast_downloader.stitch(live_mp4_file, cleartempfiles=False)
-			log_info_green('Successfully stitched downloaded files into video.')
-			try:
-				os.remove(os.path.join(live_folder_path, 'folder.lock'))
-			except Exception:
-				pass
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			if settings.clear_temp_files.title() == "True":
-				try:
-					shutil.rmtree(live_folder_path)
-				except Exception as e:
-					log_error("Could not remove temp folder: {:s}".format(str(e)))
-			log_seperator()
-			sys.exit(0)
-		except ValueError as e:
-			log_error('Could not stitch downloaded files: {:s}'.format(str(e)))
-			log_error('Likely the download duration was too short and no temp files were saved.')
-			log_seperator()
-			try:
-				os.remove(os.path.join(live_folder_path, 'folder.lock'))
-			except Exception:
-				pass
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(1)
-		except Exception as e:
-			log_error('Could not stitch downloaded files: {:s}'.format(str(e)))
-			log_seperator()
-			try:
-				os.remove(os.path.join(live_folder_path, 'folder.lock'))
-			except Exception:
-				pass
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(1)
-	except KeyboardInterrupt:
-			log_info_blue('Aborted stitching process, no video was created.')
-			log_seperator()
-			try:
-				os.remove(os.path.join(live_folder_path, 'folder.lock'))
-			except Exception:
-				pass
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(0)
-
-
-
-def get_user_info(user_to_download):
-	is_user_id = False
-	try:
-		user_id = int(user_to_download)
-		is_user_id = True
-	except ValueError:
-		user_id = user_to_download
-		try:
-			user_res = instagram_api.username_info(user_to_download)
-			user_id = user_res.get('user', {}).get('pk')
-		except ClientConnectionError as cce:
-			log_error('Could not get user info for "{:s}": {:d} {:s}'.format(user_to_download, cce.code, str(cce)))
-			if "getaddrinfo failed" in str(cce):
-				log_error('Could not resolve host, check your internet connection.')
-			if "timed out" in str(cce):
-				log_error('The connection timed out, check your internet connection.')
-			log_seperator()
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(1)
-		except ClientThrottledError as cte:
-			log_error('Could not get user info for "{:s}": {:d} {:s}.'.format(user_to_download, cte.code, str(cte)))
-			log_error('You are making too many requests at this time.')
-			log_seperator()
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(1)
-		except ClientError as ce:
-			log_error('Could not get user info for "{:s}": {:d} {:s}'.format(user_to_download, ce.code, str(ce)))
-			if ("Not Found") in str(ce):
-				log_error('The specified user does not exist.')
-			log_seperator()
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(1)
-		except Exception as e:
-			log_error('Could not get user info for "{:s}": {:s}'.format(user_to_download, str(e)))
-			log_seperator()
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(1)
-		except KeyboardInterrupt:
-			log_info_blue('Aborted getting user info for "{:s}", exiting...'.format(user_to_download))
-			log_seperator()
-			try:
-				os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-			except Exception:
-				pass
-			sys.exit(0)
-	if is_user_id:
-		log_info_green('Getting info for "{:s}" successful. (assumed input is Id)'.format(user_to_download))
-	else:
-		log_info_green('Getting info for "{:s}" successful.'.format(user_to_download))
-	get_broadcasts_info(user_id)
-
-
-
-def get_broadcasts_info(user_id):
-	try:
-		log_seperator()
-		log_info_green('Checking for livestreams and replays...')
-		log_seperator()
-		broadcasts = instagram_api.user_story_feed(user_id)
-
-		livestream = broadcasts.get('broadcast')
-		replays = broadcasts.get('post_live_item', {}).get('broadcasts', [])
-
-		if settings.save_lives.title() == "True":
-			if livestream:
-				download_livestream(livestream)
-			else:
-				log_info_green('There are no available livestreams.')
-		else:
-			log_info_blue("Livestream saving is disabled either with an argument or in the config file.")
-			
-
-		if settings.save_replays.title() == "True":
-			if replays:
-				log_seperator()
-				log_info_green('Replays found, beginning download...')
-				log_seperator()
-				download_replays(replays)
-			else:
-				log_info_green('There are no available replays.')
-		else:
-			log_seperator()
-			log_info_blue("Replay saving is disabled either with an argument or in the config file.")
-
-		log_seperator()
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-	except Exception as e:
-		log_error('Could not finish checking: {:s}'.format(str(e)))
-		if "timed out" in str(e):
-			log_error('The connection timed out, check your internet connection.')
-		log_seperator()
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-		sys.exit(1)
-	except KeyboardInterrupt:
-		log_info_blue('Aborted checking for livestreams and replays, exiting...'.format(user_to_download))
-		log_seperator()
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-		sys.exit(1)
-	except ClientThrottledError as cte:
-		log_error('Could not check because you are making too many requests at this time.')
-		log_seperator()
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-		sys.exit(1)
-
-def download_replays(broadcasts):
-	try:
-		try:
-			log_info_green('Amount of replays    : {:s}'.format(str(len(broadcasts))))
-			for replay_index, broadcast in enumerate(broadcasts):
-				bc_dash_manifest = parseString(broadcast.get('dash_manifest')).getElementsByTagName('Period')
-				bc_duration_raw = bc_dash_manifest[0].getAttribute("duration")	
-				bc_hours = (bc_duration_raw.split("PT"))[1].split("H")[0]
-				bc_minutes = (bc_duration_raw.split("H"))[1].split("M")[0]
-				bc_seconds = ((bc_duration_raw.split("M"))[1].split("S")[0]).split('.')[0]
-				log_info_green('Replay {:s} duration    : {:s} minutes and {:s} seconds'.format(str(replay_index + 1), bc_minutes, bc_seconds))
-		except Exception as e:
-			log_warn("An error occurred while getting replay duration information: {:s}".format(str(e)))
-		log_seperator()
-		log_info_green("Downloading replays... press [CTRL+C] to abort.")
-		log_seperator()
-		for replay_index, broadcast in enumerate(broadcasts):
-			exists = False
-
-			if sys.version.split(' ')[0].startswith('2'):
-				directories = (os.walk(settings.save_path).next()[1])
-			else:
-				directories = (os.walk(settings.save_path).__next__()[1])
-
-			for directory in directories:
-				if (str(broadcast.get('id')) in directory) and ("_live_" not in directory):
-					log_info_blue("Already downloaded a replay with ID '{:s}'.".format(str(broadcast.get('id'))))
-					exists = True
-			if not exists:
-				current = replay_index + 1
-				log_info_green("Downloading replay {:s} of {:s} with ID '{:s}'...".format(str(current), str(len(broadcasts)), str(broadcast.get('id'))))
-				current_time = str(int(time.time()))
-				output_dir = '{}{}_{}_{}_{}_replay_downloads'.format(settings.save_path, settings.current_date, user_to_download, broadcast.get('id'), settings.current_time)
-				broadcast_downloader = replay.Downloader(
-					mpd=broadcast.get('dash_manifest'),
-					output_dir=output_dir,
-					user_agent=instagram_api.user_agent,
-					ffmpeg_binary=settings.ffmpeg_path)
-				if settings.use_locks.title() == "True":
-					open(os.path.join(output_dir, 'folder.lock'), 'a').close()
-				replay_mp4_file = '{}{}_{}_{}_{}_replay.mp4'.format(settings.save_path, settings.current_date, user_to_download, broadcast.get('id'), settings.current_time)
-				replay_json_file = os.path.join(output_dir, '{}_{}_{}_{}_replay_comments.json'.format(settings.current_date, user_to_download, broadcast.get('id'), settings.current_time))
-
-				if settings.clear_temp_files.title() == "True":
-					replay_saved = broadcast_downloader.download(replay_mp4_file, cleartempfiles=True)
-				else:
-					replay_saved = broadcast_downloader.download(replay_mp4_file, cleartempfiles=False)
-
-				if settings.save_comments.title() == "True":
-					log_info_green("Downloading replay comments...")
-					try:
-						get_replay_comments(instagram_api, broadcast, replay_json_file, broadcast_downloader)
-					except Exception as e:
-						log_error('An error occurred while downloading comments: {:s}'.format(str(e)))
-
-				if (len(replay_saved) == 1):
-					log_info_green("Finished downloading replay {:s} of {:s}.".format(str(current), str(len(broadcasts))))
-					try:
-						os.remove(os.path.join(output_dir, 'folder.lock'))
-					except Exception:
-						pass
-
-					if (current != len(broadcasts)):
-						log_seperator()
-
-				else:
-					log_warn("No output video file was made, please merge the files manually if possible.")
-					log_warn("Check if ffmpeg is available by running ffmpeg in your terminal/cmd prompt.")
-					log_whiteline()
-
-		log_seperator()
-		log_info_green("Finished downloading all available replays.")
-		log_seperator()
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-		sys.exit(0)
-	except Exception as e:
-		log_error('Could not save replay: {:s}'.format(str(e)))
-		log_seperator()
-		try:
-			os.remove(os.path.join(output_dir, 'folder.lock'))
-		except Exception:
-			pass
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-		sys.exit(1)
-	except KeyboardInterrupt:
-		log_seperator()
-		log_info_blue('The download has been aborted by the user, exiting...')
-		log_seperator()
-		try:
-			shutil.rmtree(output_dir)
-		except Exception as e:
-			log_error("Could not remove temp folder: {:s}".format(str(e)))
-			sys.exit(1)
-		try:
-			os.remove(os.path.join(settings.save_path, user_to_download + '.lock'))
-		except Exception:
-			pass
-		sys.exit(0)
-
-
-
-def get_replay_comments(instagram_api, broadcast, comments_json_file, broadcast_downloader):
-	try:
-		comments_downloader = CommentsDownloader(
-			api=instagram_api, broadcast=broadcast, destination_file=comments_json_file)
-		comments_downloader.get_replay()
-
-		try:
-			if comments_downloader.comments:
-				comments_log_file = comments_json_file.replace('.json', '.log')
-				comment_errors, total_comments = CommentsDownloader.generate_log(
-					comments_downloader.comments, broadcast.get('published_time'), comments_log_file,
-					comments_delay=0)
-				if total_comments == 1:
-					log_info_green("Successfully saved 1 comment to logfile.")
-					log_seperator()
-					return True
-				else:
-					if comment_errors:
-						log_warn("Successfully saved {:s} comments to logfile but {:s} comments are (partially) missing.".format(str(total_comments), str(comment_errors)))
-					else:
-						log_info_green("Successfully saved {:s} comments to logfile.".format(str(total_comments)))
-					log_seperator()
-					return True
-			else:
-				log_info_green("There are no available comments to save.")
-				return False
-		except Exception as e:
-			log_error('Could not save comments to logfile: {:s}'.format(str(e)))
-			return False
-	except KeyboardInterrupt as e:
-		log_info_blue("Downloading replay comments has been aborted.")
-		return False
-
-
-
-def get_live_comments(instagram_api, broadcast, comments_json_file, broadcast_downloader):
-	try:
-		comments_downloader = CommentsDownloader(
-			api=instagram_api, broadcast=broadcast, destination_file=comments_json_file)
-		first_comment_created_at = 0
-
-		try:
-			while not broadcast_downloader.is_aborted:
-				if 'initial_buffered_duration' not in broadcast and broadcast_downloader.initial_buffered_duration:
-					broadcast['initial_buffered_duration'] = broadcast_downloader.initial_buffered_duration
-					comments_downloader.broadcast = broadcast
-				first_comment_created_at = comments_downloader.get_live(first_comment_created_at)
-		except ClientError as e:
-			if not 'media has been deleted' in e.error_response:
-				log_warn("Comment collection ClientError: %d %s" % (e.code, e.error_response))
-
-		try:
-			if comments_downloader.comments:
-				comments_downloader.save()
-				comments_log_file = comments_json_file.replace('.json', '.log')
-				comment_errors, total_comments = CommentsDownloader.generate_log(
-					comments_downloader.comments, settings.current_time, comments_log_file,
-					comments_delay=broadcast_downloader.initial_buffered_duration)
-				if len(comments_downloader.comments) == 1:
-					log_info_green("Successfully saved 1 comment to logfile.")
-					log_seperator()
-					return True
-				else:
-					if comment_errors:
-						log_warn("Successfully saved {:s} comments to logfile but {:s} comments are (partially) missing.".format(str(total_comments), str(comment_errors)))
-					else:
-						log_info_green("Successfully saved {:s} comments to logfile.".format(str(total_comments)))
-					log_seperator()
-					return True
-			else:
-				log_info_green("There are no available comments to save.")
-				return False
-				log_seperator()
-		except Exception as e:
-			log_error('Could not save comments to logfile: {:s}'.format(str(e)))
-			return False
-	except KeyboardInterrupt as e:
-		log_info_blue("Downloading livestream comments has been aborted.")
-		return False
+try:
+    import logger
+    import helpers
+    import pil
+    import dlfuncs
+except ImportError:
+    from . import logger
+    from . import helpers
+    from . import pil
+    from . import dlfuncs
+
+
+def start():
+    if pil.args.downloadfollowing:
+        if not helpers.command_exists("pyinstalive"):
+            logger.error("PyInstaLive must be properly installed when using the -df argument.")
+            logger.separator()
+        else:
+            dlfuncs.download_following()
+    else:
+        if not helpers.check_lock_file():
+            helpers.create_lock_user()
+            checking_self = pil.dl_user == pil.ig_api.authenticated_user_name
+            if dlfuncs.get_broadcasts_info():
+                if pil.dl_lives:
+                    if checking_self:
+                        logger.warn("Login with a different account to download your own livestreams.")
+                    elif pil.livestream_obj:
+                        logger.info("Livestream available, starting download.")
+                        dlfuncs.download_livestream()
+                    else:
+                        logger.info('There are no available livestreams.')
+                else:
+                    logger.binfo("Livestream downloading is disabled either with an argument or in the config file.")
+
+                logger.separator()
+
+                if pil.dl_replays:
+                    if pil.replays_obj:
+                        logger.info(
+                            '{:d} {:s} available, beginning download.'.format(len(pil.replays_obj), "replays" if len(
+                                pil.replays_obj) > 1 else "replay"))
+                        dlfuncs.download_replays()
+                    else:
+                        logger.info('There are no available replays{:s}.'.format(" saved on your account" if checking_self else ""))
+                else:
+                    logger.binfo("Replay downloading is disabled either with an argument or in the config file.")
+
+            helpers.remove_lock()
+            logger.separator()
+        else:
+            logger.warn("Lock file is already present for this user, there is probably another download ongoing.")
+            logger.warn("If this is not the case, manually delete the file '{:s}' and try again.".format(
+                pil.dl_user + '.lock'))
+            logger.separator()

+ 291 - 0
pyinstalive/helpers.py

@@ -0,0 +1,291 @@
+import time
+import subprocess
+import os
+import shutil
+import json
+import shlex
+
+try:
+    import pil
+    import helpers
+    import logger
+    from constants import Constants
+except ImportError:
+    from . import pil
+    from . import helpers
+    from . import logger
+    from .constants import Constants
+
+
+def strdatetime():
+    return time.strftime('%m-%d-%Y %I:%M:%S %p')
+
+
+def strtime():
+    return time.strftime('%I:%M:%S %p')
+
+
+def strdate():
+    return time.strftime('%m-%d-%Y')
+
+
+def strepochtime():
+    return str(int(time.time()))
+
+
+def strdatetime_compat(epochtime):
+    return time.strftime('%m%d%Y_{:s}'.format(epochtime))
+
+
+def command_exists(command):
+    try:
+        fnull = open(os.devnull, 'w')
+        subprocess.call([command], stdout=fnull, stderr=subprocess.STDOUT)
+        return True
+    except OSError as e:
+        return False
+
+
+def run_command(command):
+    try:
+        fnull = open(os.devnull, 'w')
+        subprocess.Popen(shlex.split(command), stdout=fnull, stderr=subprocess.STDOUT)
+        return False
+    except Exception as e:
+        return str(e)
+
+
+def bool_str_parse(bool_str):
+    if bool_str.lower() in ["true", "yes", "y", "1"]:
+        return True
+    elif bool_str.lower() in ["false", "no", "n", "0"]:
+        return False
+    else:
+        return "Invalid"
+
+
+def check_if_guesting():
+    try:
+        broadcast_guest = pil.livestream_obj.get('cobroadcasters', {})[0].get('username')
+    except Exception:
+        broadcast_guest = None
+    print(broadcast_guest)
+    if broadcast_guest and not pil.has_guest:
+        logger.binfo('The livestream owner has started guesting "{}".'.format(broadcast_guest))
+        pil.has_guest = broadcast_guest
+    if not broadcast_guest and pil.has_guest:
+        logger.binfo('The livestream owner has stopped guesting "{}".'.format(broadcast_guest))
+        pil.has_guest = None
+
+
+
+def generate_json_segments():
+    while not pil.broadcast_downloader.is_aborted:
+        pil.livestream_obj['delay'] = (int(pil.epochtime) - pil.livestream_obj['published_time'])
+        if 'initial_buffered_duration' not in pil.livestream_obj and pil.broadcast_downloader.initial_buffered_duration:
+            pil.livestream_obj['initial_buffered_duration'] = pil.broadcast_downloader.initial_buffered_duration
+        pil.livestream_obj['segments'] = pil.broadcast_downloader.segment_meta
+        try:
+            with open(pil.live_folder_path + ".json", 'w') as json_file:
+                json.dump(pil.livestream_obj, json_file, indent=2)
+            if not pil.broadcast_downloader.stream_id:
+                pil.broadcast_downloader.stream_id = pil.livestream_obj['id']
+            #check_if_guesting()
+            time.sleep(2.5)
+        except Exception as e:
+            logger.warn(str(e))
+
+
+def clean_download_dir():
+    dir_delcount = 0
+    file_delcount = 0
+    error_count = 0
+    lock_count = 0
+    try:
+        logger.info('Cleaning up temporary files and folders.')
+        if Constants.PYTHON_VER[0] == "2":
+            directories = (os.walk(pil.dl_path).next()[1])
+            files = (os.walk(pil.dl_path).next()[2])
+        else:
+            directories = (os.walk(pil.dl_path).__next__()[1])
+            files = (os.walk(pil.dl_path).__next__()[2])
+
+        for directory in directories:
+            if directory.endswith('_downloads'):
+                if not any(filename.endswith('.lock') for filename in
+                           os.listdir(os.path.join(pil.dl_path, directory))):
+                    try:
+                        shutil.rmtree(os.path.join(pil.dl_path, directory))
+                        dir_delcount += 1
+                    except Exception as e:
+                        logger.error("Could not remove folder: {:s}".format(str(e)))
+                        error_count += 1
+                else:
+                    lock_count += 1
+        logger.separator()
+        for file in files:
+            if file.endswith('_downloads.json'):
+                if not any(filename.endswith('.lock') for filename in
+                           os.listdir(os.path.join(pil.dl_path))):
+                    try:
+                        os.remove(os.path.join(pil.dl_path, file))
+                        file_delcount += 1
+                    except Exception as e:
+                        logger.error("Could not remove file: {:s}".format(str(e)))
+                        error_count += 1
+                else:
+                    lock_count += 1
+        if dir_delcount == 0 and file_delcount == 0 and error_count == 0 and lock_count == 0:
+            logger.info('The cleanup has finished. No items were removed.')
+            logger.separator()
+            return
+        logger.info('The cleanup has finished.')
+        logger.info('Folders removed:     {:d}'.format(dir_delcount))
+        logger.info('Files removed:       {:d}'.format(file_delcount))
+        logger.info('Locked items:        {:d}'.format(lock_count))
+        logger.info('Errors:              {:d}'.format(error_count))
+        logger.separator()
+    except KeyboardInterrupt as e:
+        logger.separator()
+        logger.warn("The cleanup has been aborted.")
+        if dir_delcount == 0 and file_delcount == 0 and error_count == 0 and lock_count == 0:
+            logger.info('No items were removed.')
+            logger.separator()
+            return
+        logger.info('Folders removed:     {:d}'.format(dir_delcount))
+        logger.info('Files removed:       {:d}'.format(file_delcount))
+        logger.info('Locked items  :      {:d}'.format(lock_count))
+        logger.info('Errors:              {:d}'.format(error_count))
+        logger.separator()
+
+
+def show_info():
+    cookie_files = []
+    cookie_from_config = ''
+    try:
+        for file in os.listdir(os.getcwd()):
+            if file.endswith(".json"):
+                with open(file) as data_file:
+                    try:
+                        json_data = json.load(data_file)
+                        if json_data.get('created_ts'):
+                            cookie_files.append(file)
+                    except Exception as e:
+                        pass
+            if pil.ig_user == file.replace(".json", ''):
+                cookie_from_config = file
+    except Exception as e:
+        logger.warn("Could not check for cookie files: {:s}".format(str(e)))
+        logger.whiteline()
+    logger.info("To see all the available arguments, use the -h argument.")
+    logger.whiteline()
+    logger.info("PyInstaLive version:        {:s}".format(Constants.SCRIPT_VER))
+    logger.info("Python version:             {:s}".format(Constants.PYTHON_VER))
+    if not command_exists("ffmpeg"):
+        logger.error("FFmpeg framework:        Not found")
+    else:
+        logger.info("FFmpeg framework:           Available")
+
+    if len(cookie_from_config) > 0:
+        logger.info("Cookie files:               {:s} ({:s} matches config user)".format(str(len(cookie_files)),
+                                                                                         cookie_from_config))
+    elif len(cookie_files) > 0:
+        logger.info("Cookie files:               {:s}".format(str(len(cookie_files))))
+    else:
+        logger.warn("Cookie files:               None found")
+
+    logger.info("CLI supports color:         {:s}".format("No" if not logger.supports_color() else "Yes"))
+    logger.info(
+        "Command to run at start:    {:s}".format("None" if not pil.run_at_start else pil.run_at_start))
+    logger.info(
+        "Command to run at finish:   {:s}".format("None" if not pil.run_at_finish else pil.run_at_finish))
+
+    if os.path.exists(pil.config_path):
+        logger.info("Config file contents:")
+        logger.whiteline()
+        with open(pil.config_path) as f:
+            for line in f:
+                logger.plain("    {:s}".format(line.rstrip()))
+    else:
+        logger.error("Config file:         Not found")
+    logger.whiteline()
+    logger.info("End of PyInstaLive information screen.")
+    logger.separator()
+
+
+def new_config():
+    try:
+        if os.path.exists(pil.config_path):
+            logger.info("A configuration file is already present:")
+            logger.whiteline()
+            with open(pil.config_path) as f:
+                for line in f:
+                    logger.plain("    {:s}".format(line.rstrip()))
+            logger.whiteline()
+            logger.info("To create a default config file, delete 'pyinstalive.ini' and run this script again.")
+            logger.separator()
+        else:
+            try:
+                logger.warn("Could not find configuration file, creating a default one.")
+                config_file = open(pil.config_path, "w")
+                config_file.write(Constants.CONFIG_TEMPLATE.format(os.getcwd()).strip())
+                config_file.close()
+                logger.warn("Edit the created 'pyinstalive.ini' file and run this script again.")
+                logger.separator()
+                return
+            except Exception as e:
+                logger.error("Could not create default config file: {:s}".format(str(e)))
+                logger.warn("You must manually create and edit it with the following template: ")
+                logger.whiteline()
+                for line in Constants.CONFIG_TEMPLATE.strip().splitlines():
+                    logger.plain("    {:s}".format(line.rstrip()))
+                logger.whiteline()
+                logger.warn("Save it as 'pyinstalive.ini' and run this script again.")
+                logger.separator()
+    except Exception as e:
+        logger.error("An error occurred: {:s}".format(str(e)))
+        logger.warn(
+            "If you don't have a configuration file, manually create and edit one with the following template:")
+        logger.whiteline()
+        logger.plain(Constants.CONFIG_TEMPLATE)
+        logger.whiteline()
+        logger.warn("Save it as 'pyinstalive.ini' and run this script again.")
+        logger.separator()
+
+
+def create_lock_user():
+    try:
+        if not os.path.isfile(os.path.join(pil.dl_path, pil.dl_user + '.lock')):
+            if pil.use_locks:
+                open(os.path.join(pil.dl_path, pil.dl_user + '.lock'), 'a').close()
+                return True
+        else:
+            return False
+    except Exception as e:
+        logger.warn("Lock file could not be created. Be careful when running multiple downloads concurrently!")
+        return True
+
+
+def create_lock_folder():
+    try:
+        if not os.path.isfile(os.path.join(pil.live_folder_path, 'folder.lock')):
+            if pil.use_locks:
+                open(os.path.join(pil.live_folder_path, 'folder.lock'), 'a').close()
+                return True
+        else:
+            return False
+    except Exception as e:
+        logger.warn("Lock file could not be created. Be careful when running multiple downloads concurrently!")
+        return True
+
+
+def remove_lock():
+    try:
+        os.remove(os.path.join(pil.dl_path, pil.dl_user + '.lock'))
+        os.remove(os.path.join(pil.live_folder_path, 'folder.lock'))
+    except Exception:
+        pass
+
+
+def check_lock_file():
+    return os.path.isfile(os.path.join(pil.dl_path, pil.dl_user + '.lock'))

+ 0 - 576
pyinstalive/initialize.py

@@ -1,576 +0,0 @@
-import argparse
-import configparser
-import logging
-import os.path
-import subprocess
-import sys
-import time
-import shutil
-import json
-
-from .auth import login
-from .downloader import start_single, start_multiple
-from .logger import log_seperator, supports_color, log_info_blue, log_info_green, log_warn, log_error, log_whiteline, log_plain
-from .settings import settings
-
-script_version = "2.6.0"
-python_version = sys.version.split(' ')[0]
-bool_values = {'True', 'False'}
-
-def check_ffmpeg():
-	try:
-		FNULL = open(os.devnull, 'w')
-		if settings.ffmpeg_path == None:
-			subprocess.call(["ffmpeg"], stdout=FNULL, stderr=subprocess.STDOUT)
-		else:
-			subprocess.call([settings.ffmpeg_path], stdout=FNULL, stderr=subprocess.STDOUT)
-		return True
-	except OSError as e:
-		return False
-
-def check_pyinstalive():
-	try:
-		FNULL = open(os.devnull, 'w')
-		subprocess.call(["pyinstalive"], stdout=FNULL, stderr=subprocess.STDOUT)
-		return True
-	except OSError as e:
-		return False
-
-
-def check_config_validity(config, args=None):
-	try:
-		has_thrown_errors = False
-		settings.username = config.get('pyinstalive', 'username')
-		settings.password = config.get('pyinstalive', 'password')
-
-		try:
-			settings.use_locks = config.get('pyinstalive', 'use_locks').title()
-			if not settings.use_locks in bool_values:
-				log_warn("Invalid or missing setting detected for 'use_locks', using default value (True)")
-				settings.use_locks = 'true'
-				has_thrown_errors = True
-		except:
-			log_warn("Invalid or missing setting detected for 'use_locks', using default value (True)")
-			settings.use_locks = 'true'
-			has_thrown_errors = True
-
-		try:
-			settings.show_cookie_expiry = config.get('pyinstalive', 'show_cookie_expiry').title()
-			if not settings.show_cookie_expiry in bool_values:
-				log_warn("Invalid or missing setting detected for 'show_cookie_expiry', using default value (True)")
-				settings.show_cookie_expiry = 'true'
-				has_thrown_errors = True
-		except:
-			log_warn("Invalid or missing setting detected for 'show_cookie_expiry', using default value (True)")
-			settings.show_cookie_expiry = 'true'
-			has_thrown_errors = True
-
-
-
-		try:
-			settings.clear_temp_files = config.get('pyinstalive', 'clear_temp_files').title()
-			if not settings.clear_temp_files in bool_values:
-				log_warn("Invalid or missing setting detected for 'clear_temp_files', using default value (True)")
-				settings.clear_temp_files = 'true'
-				has_thrown_errors = True
-		except:
-			log_warn("Invalid or missing setting detected for 'clear_temp_files', using default value (True)")
-			settings.clear_temp_files = 'true'
-			has_thrown_errors = True
-
-
-
-		try:
-			settings.save_replays = config.get('pyinstalive', 'save_replays').title()
-			if not settings.save_replays in bool_values:
-				log_warn("Invalid or missing setting detected for 'save_replays', using default value (True)")
-				settings.save_replays = 'true'
-				has_thrown_errors = True
-		except:
-			log_warn("Invalid or missing setting detected for 'save_replays', using default value (True)")
-			settings.save_replays = 'true'
-			has_thrown_errors = True
-
-		try:
-			settings.save_lives = config.get('pyinstalive', 'save_lives').title()
-			if not settings.save_lives in bool_values:
-				log_warn("Invalid or missing setting detected for 'save_lives', using default value (True)")
-				settings.save_lives = 'true'
-				has_thrown_errors = True
-		except:
-			log_warn("Invalid or missing setting detected for 'save_lives', using default value (True)")
-			settings.save_lives = 'true'
-			has_thrown_errors = True
-
-
-
-		try:
-			settings.log_to_file = config.get('pyinstalive', 'log_to_file').title()
-			if not settings.log_to_file in bool_values:
-				log_warn("Invalid or missing setting detected for 'log_to_file', using default value (False)")
-				settings.log_to_file = 'False'
-				has_thrown_errors = True
-		except:
-			log_warn("Invalid or missing setting detected for 'log_to_file', using default value (False)")
-			settings.log_to_file = 'False'
-			has_thrown_errors = True
-
-
-
-		try:
-			settings.run_at_start = config.get('pyinstalive', 'run_at_start').replace("\\", "\\\\")
-			if not settings.run_at_start:
-				settings.run_at_start = "None"
-		except:
-			log_warn("Invalid or missing settings detected for 'run_at_start', using default value (empty)")
-			settings.run_at_start = "None"
-			has_thrown_errors = True
-
-
-
-		try:
-			settings.run_at_finish = config.get('pyinstalive', 'run_at_finish').replace("\\", "\\\\")
-			if not settings.run_at_finish:
-				settings.run_at_finish = "None"
-		except:
-			log_warn("Invalid or missing settings detected for 'run_at_finish', using default value (empty)")
-			settings.run_at_finish = "None"
-			has_thrown_errors = True
-
-
-		try:
-			settings.save_comments = config.get('pyinstalive', 'save_comments').title()
-			wide_build = sys.maxunicode > 65536
-			if sys.version.split(' ')[0].startswith('2') and settings.save_comments == "True" and not wide_build:
-				log_warn("Your Python 2 build does not support wide unicode characters.")
-				log_warn("This means characters such as emojis will not be saved.")
-				has_thrown_errors = True
-			else:
-				if not settings.show_cookie_expiry in bool_values:
-					log_warn("Invalid or missing setting detected for 'save_comments', using default value (False)")
-					settings.save_comments = 'false'
-					has_thrown_errors = True
-		except:
-			log_warn("Invalid or missing setting detected for 'save_comments', using default value (False)")
-			settings.save_comments = 'false'
-			has_thrown_errors = True
-
-		try:
-			if args and args.savepath:
-				args.savepath = args.savepath.replace("\"", "").replace("'", "")
-				if (os.path.exists(args.savepath)) and (args.savepath != config.get('pyinstalive', 'save_path')):
-					settings.save_path = args.savepath
-					log_info_blue("Overriding save path: {:s}".format(args.savepath))
-					log_seperator()
-				elif (args.savepath != config.get('pyinstalive', 'save_path')):
-					log_warn("Custom save path does not exist, falling back to path specified in config.")
-					settings.save_path = config.get('pyinstalive', 'save_path')
-					log_seperator()
-				else:
-					settings.save_path = config.get('pyinstalive', 'save_path')
-			else:
-				settings.save_path = config.get('pyinstalive', 'save_path')
-
-			if not settings.save_path.endswith('/'):
-				settings.save_path = settings.save_path + '/'
-
-			if (os.path.exists(settings.save_path)):
-				pass
-			else:
-				log_warn("Invalid or missing setting detected for 'save_path', falling back to path: {:s}".format(os.getcwd()))
-				settings.save_path = os.getcwd()
-				has_thrown_errors = True
-				if not settings.save_path.endswith('/'):
-					settings.save_path = settings.save_path + '/'
-		except Exception as e:
-			log_warn("Invalid or missing setting detected for 'save_path', falling back to path: {:s}".format(os.getcwd()))
-			settings.save_path = os.getcwd()
-			has_thrown_errors = True
-			if not settings.save_path.endswith('/'):
-				settings.save_path = settings.save_path + '/'
-
-
-		try:
-			settings.ffmpeg_path = config.get('pyinstalive', 'ffmpeg_path')
-			if (len(settings.ffmpeg_path) > 1) and (os.path.exists(settings.ffmpeg_path)):
-				log_info_blue("Overriding FFmpeg path: {:s}".format(settings.ffmpeg_path))
-				log_seperator()
-			else:
-				if (len(settings.ffmpeg_path) > 1):
-					log_warn("Invalid setting detected for 'ffmpeg_path', falling back to environment variable.")
-					has_thrown_errors = True
-				settings.ffmpeg_path = None
-		except:
-			log_warn("Invalid or missing setting detected for 'ffmpeg_path', falling back to environment variable.")
-			settings.ffmpeg_path = None
-			has_thrown_errors = True
-
-
-		if has_thrown_errors:
-			log_seperator()
-
-		return True
-	except Exception as e:
-		print(str(e))
-		return False
-
-
-
-def show_info(config):
-	if os.path.exists(settings.custom_config_path):
-		try:
-			config.read(settings.custom_config_path)
-		except Exception as e:
-			log_error("Could not read configuration file: {:s}".format(str(e)))
-			log_seperator()
-	else:
-		new_config()
-		sys.exit(1)
-
-	if not check_config_validity(config):
-		log_warn("Config file is not valid, some information may be inaccurate.")
-		log_whiteline()
-
-	cookie_files = []
-	cookie_from_config = ''
-	try:
-		for file in os.listdir(os.getcwd()):
-			if file.endswith(".json"):
-				with open(file) as data_file:    
-					try:
-						json_data = json.load(data_file)
-						if (json_data.get('created_ts')):
-							cookie_files.append(file)
-					except Exception as e:
-						pass
-			if settings.username == file.replace(".json", ''):
-				cookie_from_config = file
-	except Exception as e:
-		log_warn("Could not check for cookie files: {:s}".format(str(e)))
-		log_whiteline()
-	log_info_green("To see all the available arguments, use the -h argument.")
-	log_whiteline()
-	log_info_green("PyInstaLive version:    	{:s}".format(script_version))
-	log_info_green("Python version:         	{:s}".format(python_version))
-	if not check_ffmpeg():
-		log_error("FFmpeg framework:       	Not found")
-	else:
-		log_info_green("FFmpeg framework:       	Available")
-
-	if (len(cookie_from_config) > 0):
-		log_info_green("Cookie files:            	{:s} ({:s} matches config user)".format(str(len(cookie_files)), cookie_from_config))
-	elif len(cookie_files) > 0:
-		log_info_green("Cookie files:            	{:s}".format(str(len(cookie_files))))
-	else:
-		log_warn("Cookie files:            	None found")
-
-	log_info_green("CLI supports color:     	{:s}".format(str(supports_color()[0])))
-	log_info_green("File to run at start:       {:s}".format(settings.run_at_start))
-	log_info_green("File to run at finish:      {:s}".format(settings.run_at_finish))
-	log_whiteline()
-
-
-	if os.path.exists(settings.custom_config_path):
-		log_info_green("Config file:")
-		log_whiteline()
-		with open(settings.custom_config_path) as f:
-			for line in f:
-				log_plain("    {:s}".format(line.rstrip()))
-	else:
-		log_error("Config file:	    	Not found")
-	log_whiteline()
-	log_info_green("End of PyInstaLive information screen.")
-	log_seperator()
-
-
-
-def new_config():
-	try:
-		if os.path.exists(settings.custom_config_path):
-			log_info_green("A configuration file is already present:")
-			log_whiteline()
-			with open(settings.custom_config_path) as f:
-				for line in f:
-					log_plain("    {:s}".format(line.rstrip()))
-			log_whiteline()
-			log_info_green("To create a default config file, delete 'pyinstalive.ini' and run this script again.")
-			log_seperator()
-		else:
-			try:
-				log_warn("Could not find configuration file, creating a default one...")
-				config_template = """
-[pyinstalive]
-username = johndoe
-password = grapefruits
-save_path = {:s}
-ffmpeg_path = 
-show_cookie_expiry = true
-clear_temp_files = false
-save_lives = true
-save_replays = true
-run_at_start = 
-run_at_finish = 
-save_comments = false
-log_to_file = false
-use_locks = true
-				""".format(os.getcwd())
-				config_file = open(settings.custom_config_path, "w")
-				config_file.write(config_template.strip())
-				config_file.close()
-				log_warn("Edit the created 'pyinstalive.ini' file and run this script again.")
-				log_seperator()
-				sys.exit(0)
-			except Exception as e:
-				log_error("Could not create default config file: {:s}".format(str(e)))
-				log_warn("You must manually create and edit it with the following template: ")
-				log_whiteline()
-				for line in config_template.strip().splitlines():
-					log_plain("    {:s}".format(line.rstrip()))
-				log_whiteline()
-				log_warn("Save it as 'pyinstalive.ini' and run this script again.")
-				log_seperator()
-	except Exception as e:
-		log_error("An error occurred: {:s}".format(str(e)))
-		log_warn("If you don't have a configuration file, manually create and edit one with the following template:")
-		log_whiteline()
-		log_plain(config_template)
-		log_whiteline()
-		log_warn("Save it as 'pyinstalive.ini' and run this script again.")
-		log_seperator()
-
-
-
-def clean_download_dir():
-	dir_delcount = 0
-	error_count = 0
-	lock_count = 0
-
-	log_info_green('Cleaning up temporary files and folders...')
-	try:
-		if sys.version.split(' ')[0].startswith('2'):
-			directories = (os.walk(settings.save_path).next()[1])
-			files = (os.walk(settings.save_path).next()[2])
-		else:
-			directories = (os.walk(settings.save_path).__next__()[1])
-			files = (os.walk(settings.save_path).__next__()[2])
-
-		for directory in directories:
-			if directory.endswith('_downloads'):
-				if not any(filename.endswith('.lock') for filename in os.listdir(settings.save_path + directory)):
-					try:
-						shutil.rmtree(settings.save_path + directory)
-						dir_delcount += 1
-					except Exception as e:
-						log_error("Could not remove temp folder: {:s}".format(str(e)))
-						error_count += 1
-				else:
-					lock_count += 1
-		log_seperator()
-		log_info_green('The cleanup has finished.')
-		if dir_delcount == 0 and error_count == 0 and lock_count == 0:
-			log_info_green('No folders were removed.')
-			log_seperator()
-			return
-		log_info_green('Folders removed:     {:d}'.format(dir_delcount))
-		log_info_green('Locked folders:      {:d}'.format(lock_count))
-		log_info_green('Errors:              {:d}'.format(error_count))
-		log_seperator()
-	except KeyboardInterrupt as e:
-		log_seperator()
-		log_warn("The cleanup has been aborted.")
-		if dir_delcount == 0 and error_count == 0 and lock_count == 0:
-			log_info_green('No folders were removed.')
-			log_seperator()
-			return
-		log_info_green('Folders removed:     {:d}'.format(dir_delcount))
-		log_info_green('Locked folders:      {:d}'.format(lock_count))
-		log_info_green('Errors:              {:d}'.format(error_count))
-		log_seperator()
-
-def run():
-	logging.disable(logging.CRITICAL)
-	config = configparser.ConfigParser()
-	parser = argparse.ArgumentParser(description="You are running PyInstaLive {:s} using Python {:s}".format(script_version, python_version))
-	parser.add_argument('-u', '--username', dest='username', type=str, required=False, help="Instagram username to login with.")
-	parser.add_argument('-p', '--password', dest='password', type=str, required=False, help="Instagram password to login with.")
-	parser.add_argument('-r', '--record', dest='download', type=str, required=False, help="The username of the user whose livestream or replay you want to save.")
-	parser.add_argument('-d', '--download', dest='download', type=str, required=False, help="The username of the user whose livestream or replay you want to save.")
-	parser.add_argument('-i', '--info', dest='info', action='store_true', help="View information about PyInstaLive.")
-	parser.add_argument('-c', '--config', dest='config', action='store_true', help="Create a default configuration file if it doesn't exist.")
-	parser.add_argument('-nr', '--noreplays', dest='noreplays', action='store_true', help="When used, do not check for any available replays.")
-	parser.add_argument('-nl', '--nolives', dest='nolives', action='store_true', help="When used, do not check for any available livestreams.")
-	parser.add_argument('-cl', '--clean', dest='clean', action='store_true', help="PyInstaLive will clean the current download folder of all leftover files.")
-	parser.add_argument('-df', '--downloadfollowing', dest='downloadfollowing', action='store_true', help="PyInstaLive will check for available livestreams and replays from users the account used to login follows.")
-	parser.add_argument('-cp', '--configpath', dest='configpath', type=str, required=False, help="Path to a PyInstaLive configuration file.")
-	parser.add_argument('-sp', '--savepath', dest='savepath', type=str, required=False, help="Path to folder where PyInstaLive should save livestreams and replays.")
-
-	# Workaround to 'disable' argument abbreviations
-	parser.add_argument('--usernamx', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('--passworx', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('--recorx', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('--infx', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('--confix', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('--noreplayx', help=argparse.SUPPRESS, metavar='IGNORE') 
-	parser.add_argument('--cleax', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('--downloadfollowinx', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('--configpatx', help=argparse.SUPPRESS, metavar='IGNORE')
-
-
-	parser.add_argument('-cx', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('-nx', help=argparse.SUPPRESS, metavar='IGNORE')
-	parser.add_argument('-dx', help=argparse.SUPPRESS, metavar='IGNORE')
-
-
-	args, unknown_args = parser.parse_known_args()
-
-
-	if args.configpath:
-		args.configpath = args.configpath.replace("\"", "").replace("'", "")
-		if os.path.exists(args.configpath):
-			settings.custom_config_path = args.configpath
-
-	try:
-		config.read(settings.custom_config_path)
-		settings.log_to_file = config.get('pyinstalive', 'log_to_file').title()
-		if not settings.log_to_file in bool_values:
-			settings.log_to_file = 'False'
-		elif settings.log_to_file == "True":
-			if args.download:
-				settings.user_to_download = args.download
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write("\n")
-					f.close()
-			except:
-				pass
-	except Exception as e:
-		settings.log_to_file = 'False'
-		pass # Pretend nothing happened
-
-	log_seperator()
-	log_info_blue('PYINSTALIVE (SCRIPT V{:s} - PYTHON V{:s}) - {:s}'.format(script_version, python_version, time.strftime('%I:%M:%S %p')))
-	log_seperator()
-
-	if args.configpath and settings.custom_config_path != 'pyinstalive.ini':
-		log_info_blue("Overriding config path: {:s}".format(args.configpath))
-		log_seperator()
-	elif args.configpath and settings.custom_config_path == 'pyinstalive.ini':
-		log_warn("Custom config path does not exist, falling back to path: {:s}".format(os.getcwd()))
-		log_seperator()
-
-	if unknown_args:
-		log_error("The following invalid argument(s) were provided: ") 
-		log_whiteline() 
-		log_info_blue('    ' + ' '.join(unknown_args)) 
-		log_whiteline()
-		if (supports_color()[1] == True):
-			log_info_green("'pyinstalive -h' can be used to display command help.")
-		else:
-			log_plain("pyinstalive -h can be used to display command help.")
-		log_seperator()
-		exit(1)
-
-	if (args.info) or (not
-	args.username and not
-	args.password and not
-	args.download and not
-	args.downloadfollowing and not
-	args.info and not
-	args.config and not
-	args.noreplays and not
-	args.nolives and not
-	args.clean and not
-	args.configpath and not
-	args.savepath):
-		show_info(config)
-		sys.exit(0)
-	
-	if (args.config):
-		new_config()
-		sys.exit(0)
-
-	if os.path.exists(settings.custom_config_path):
-		try:
-			config.read(settings.custom_config_path)
-		except Exception:
-			log_error("Could not read configuration file.")
-			log_seperator()
-	else:
-		new_config()
-		sys.exit(1)
-
-
-	if check_config_validity(config, args):
-		try:
-			if (args.clean):
-				clean_download_dir()
-				sys.exit(0)
-
-			if not check_ffmpeg():
-				log_error("Could not find ffmpeg, the script will now exit. ")
-				log_seperator()
-				sys.exit(1)
-
-			if (args.noreplays):
-				settings.save_replays = "False"
-
-			if (args.nolives):
-				settings.save_lives = "False"
-
-			if settings.save_lives == "False" and settings.save_replays == "False":
-				log_warn("Script will not run because both live and replay saving is disabled.")
-				log_seperator()
-				sys.exit(1)
-
-			if not args.download and not args.downloadfollowing:
-				log_warn("Neither argument -d or -df was passed. Please use one of the two and try again.")
-				log_seperator()
-				sys.exit(1)
-
-			if args.download and args.downloadfollowing:
-				log_warn("You can't pass both the -d and -df arguments. Please use one of the two and try again.")
-				log_seperator()
-				sys.exit(1)
-
-
-			if (args.username is not None) and (args.password is not None):
-				api = login(args.username, args.password, settings.show_cookie_expiry, True)
-			elif (args.username is not None) or (args.password is not None):
-				log_warn("Missing --username or --password argument, falling back to configuration file...")
-				if (not len(settings.username) > 0) or (not len(settings.password) > 0):
-					log_error("Username or password are missing. Please check your configuration file and try again.")
-					log_seperator()
-					sys.exit(1)
-				else:
-					api = login(settings.username, settings.password, settings.show_cookie_expiry, False)
-			else:
-				if (not len(settings.username) > 0) or (not len(settings.password) > 0):
-					log_error("Username or password are missing. Please check your configuration file and try again.")
-					log_seperator()
-					sys.exit(1)
-				else:
-					api = login(settings.username, settings.password, settings.show_cookie_expiry, False)
-			if args.download and not args.downloadfollowing:
-				start_single(api, args.download, settings)
-			if not args.download and args.downloadfollowing:
-				if settings.use_locks.title() == "False":
-					log_warn("The use of lock files is disabled, this might cause trouble!")
-					log_seperator()
-				if check_pyinstalive():
-					start_multiple(api, settings, "pyinstalive", args)
-				else:
-					log_warn("You probably ran PyInstaLive as a script module with the -m argument.")
-					log_warn("PyInstaLive should be properly installed when using the -df argument.")
-					log_seperator()
-					if python_version[0] == "3":
-						start_multiple(api, settings, "python3 -m pyinstalive", args)
-					else:
-						start_multiple(api, settings, "python -m pyinstalive", args)
-		except KeyboardInterrupt as ee:
-			log_warn("Pre-download checks have been aborted, exiting...")
-			log_seperator()
-
-	else:
-		log_error("The configuration file is not valid. Please double-check and try again.")
-		log_seperator()
-		sys.exit(1)

+ 109 - 138
pyinstalive/logger.py

@@ -1,143 +1,114 @@
 import os
 import sys
-import re
-from .settings import settings
 
-sep = "-" * 70
+try:
+    import pil
+    import helpers
+    from constants import Constants
+except ImportError:
+    from . import pil
+    from . import helpers
+    from .constants import Constants
+
 
 def supports_color():
-	try:
-		"""
-		from https://github.com/django/django/blob/master/django/core/management/color.py
-		Return True if the running system's terminal supports color,
-		and False otherwise.
-		"""
-
-		plat = sys.platform
-		supported_platform = plat != 'Pocket PC' and (plat != 'win32' or 'ANSICON' in os.environ)
-
-		# isatty is not always implemented, #6223.
-		is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
-		if not supported_platform or not is_a_tty:
-			return "No", False
-		return "Yes", True
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
-
-
-
-def log_seperator():
-	try:
-		print(sep)
-		if settings.log_to_file == 'True':
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write(sep + '\n')
-					f.close()
-			except:
-				pass
-		sys.stdout.flush()
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
-
-
-def log_info_green(string):
-	try:
-		if supports_color()[1] == False:
-			print(string)
-		else:
-			print('\x1B[1;32;40m[I]\x1B[0m {:s}\x1B[0m'.format(string))
-		if settings.log_to_file == 'True':
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write("[I] {:s}\n".format(string))
-					f.close()
-			except:
-				pass
-		sys.stdout.flush()
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
-
-
-def log_info_blue(string):
-	try:
-		if supports_color()[1] == False:
-			print(string)
-		else:
-			print('\x1B[1;34;40m[I]\x1B[0m {:s}\x1B[0m'.format(string))
-		if settings.log_to_file == 'True':
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write("[I] {:s}\n".format(string))
-					f.close()
-			except:
-				pass
-		sys.stdout.flush()
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
-
-
-def log_warn(string):
-	try:
-		if supports_color()[1] == False:
-			print(string)
-		else:
-			print('\x1B[1;33;40m[W]\x1B[0m {:s}\x1B[0m'.format(string))
-		if settings.log_to_file == 'True':
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write("[W] {:s}\n".format(string))
-					f.close()
-			except:
-				pass
-		sys.stdout.flush()
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
-
-
-def log_error(string):
-	try:
-		if supports_color()[1] == False:
-			print(string)
-		else:
-			print('\x1B[1;31;40m[E]\x1B[0m {:s}\x1B[0m'.format(string))
-		if settings.log_to_file == 'True':
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write("[E] {:s}\n".format(string))
-					f.close()
-			except:
-				pass
-		sys.stdout.flush()
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
-
-
-def log_whiteline():
-	try:
-		print("")
-		if settings.log_to_file == 'True':
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write("\n")
-					f.close()
-			except:
-				pass
-		sys.stdout.flush()
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
-
-
-def log_plain(string):
-	try:
-		print(string)
-		if settings.log_to_file == 'True':
-			try:
-				with open("pyinstalive{:s}.log".format("_" + settings.user_to_download if len(settings.user_to_download) > 0 else ".default"),"a+") as f:
-					f.write("{:s}\n".format(string))
-					f.close()
-			except:
-				pass
-		sys.stdout.flush()
-	except Exception as e:
-		print("Error while logging: {}" + str(e))
+    try:
+        """
+        from https://github.com/django/django/blob/master/django/core/management/color.py
+        Return True if the running system's terminal supports color,
+        and False otherwise.
+        """
+
+        plat = sys.platform
+        supported_platform = plat != 'Pocket PC' and (plat != 'win32' or 'ANSICON' in os.environ)
+
+        # isatty is not always implemented, #6223.
+        is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
+        if not supported_platform or not is_a_tty:
+            return False
+        return True
+    except Exception:
+        return False
+
+
+PREFIX_ERROR = '\x1B[1;31;40m[E]\x1B[0m'
+PREFIX_INFO = '\x1B[1;32;40m[I]\x1B[0m'
+PREFIX_WARN = '\x1B[1;33;40m[W]\x1B[0m'
+PREFIX_BINFO = '\x1B[1;34;40m[I]\x1B[0m'
+PRINT_SEP = '-' * 75
+SUPP_COLOR = supports_color()
+
+
+def info(log_str, force_plain=False):
+    if SUPP_COLOR and not force_plain:
+        to_print = "{:s} {:s}".format(PREFIX_INFO, log_str)
+    else:
+        to_print = "[I] {:s}".format(log_str)
+    print(to_print)
+    if pil.log_to_file:
+        _log_to_file(log_str)
+
+
+def binfo(log_str, force_plain=False):
+    if SUPP_COLOR and not force_plain:
+        to_print = "{:s} {:s}".format(PREFIX_BINFO, log_str)
+    else:
+        to_print = "[I] {:s}".format(log_str)
+    print(to_print)
+    if pil.log_to_file:
+        _log_to_file(log_str)
+
+
+def warn(log_str, force_plain=False):
+    if SUPP_COLOR and not force_plain:
+        to_print = "{:s} {:s}".format(PREFIX_WARN, log_str)
+    else:
+        to_print = "[W] {:s}".format(log_str)
+    print(to_print)
+    if pil.log_to_file:
+        _log_to_file(log_str)
+
+
+def error(log_str, force_plain=False):
+    if SUPP_COLOR and not force_plain:
+        to_print = "{:s} {:s}".format(PREFIX_ERROR, log_str)
+    else:
+        to_print = "[E] {:s}".format(log_str)
+    print(to_print)
+    if pil.log_to_file:
+        _log_to_file(log_str)
+
+
+def plain(log_str):
+    print("{:s}".format(log_str))
+    if pil.log_to_file:
+        _log_to_file("{:s}".format(log_str))
+
+
+def whiteline():
+    print("")
+    if pil.log_to_file:
+        _log_to_file("")
+
+
+def separator():
+    print(PRINT_SEP)
+    if pil.log_to_file:
+        _log_to_file(PRINT_SEP)
+
+
+def banner():
+    separator()
+    binfo("PYINSTALIVE (SCRIPT V{:s} - PYTHON V{:s}) - {:s}".format(Constants.SCRIPT_VER, Constants.PYTHON_VER,
+                                                                    helpers.strdatetime()))
+    separator()
+
+
+def _log_to_file(log_str):
+    try:
+        with open("pyinstalive{:s}.log".format(
+                "_" + pil.dl_user if len(pil.dl_user) > 0 else ".default"), "a+") as f:
+            f.write(log_str + '\n')
+            f.close()
+    except Exception:
+        pass

+ 72 - 0
pyinstalive/pil.py

@@ -0,0 +1,72 @@
+try:
+    import logger
+    import helpers
+except ImportError:
+    from . import logger
+    from . import helpers
+import os
+
+
+def noinit(self):
+    pass
+
+
+def initialize():
+    global ig_api
+    global ig_user
+    global ig_pass
+    global dl_user
+    global dl_path
+    global dl_lives
+    global dl_replays
+    global dl_comments
+    global log_to_file
+    global run_at_start
+    global run_at_finish
+    global show_cookie_expiry
+    global config_path
+    global config
+    global args
+    global uargs
+    global livestream_obj
+    global replays_obj
+    global broadcast_downloader
+    global epochtime
+    global datetime_compat
+    global live_folder_path
+    global use_locks
+    global comment_thread_worker
+    global segments_json_thread_worker
+    global assemble_arg
+    global ffmpeg_path
+    global clear_temp_files
+    global has_guest
+    ig_api = None
+    ig_user = ""
+    ig_pass = ""
+    dl_user = ""
+    dl_path = os.getcwd()
+    dl_lives = True
+    dl_replays = True
+    dl_comments = True
+    log_to_file = True
+    run_at_start = ""
+    run_at_finish = ""
+    show_cookie_expiry = False
+    config_path = os.path.join(os.getcwd(), "pyinstalive.ini")
+    config = None
+    args = None
+    uargs = None
+    livestream_obj = None
+    replays_obj = None
+    broadcast_downloader = None
+    epochtime = helpers.strepochtime()
+    datetime_compat = helpers.strdatetime_compat(epochtime)
+    live_folder_path = ""
+    use_locks = True
+    comment_thread_worker = None
+    segments_json_thread_worker = None
+    assemble_arg = None
+    ffmpeg_path = None
+    clear_temp_files = False
+    has_guest = None

+ 0 - 21
pyinstalive/settings.py

@@ -1,21 +0,0 @@
-import time
-import os
-
-class settings:
-	user_to_download = ""
-	username = ""
-	password = ""
-	save_path = os.getcwd()
-	ffmpeg_path = None
-	show_cookie_expiry = "true"
-	clear_temp_files = "false"
-	current_time = str(int(time.time()))
-	current_date = time.strftime("%Y%m%d")
-	save_replays = "true"
-	save_lives = "true"
-	run_at_start = "None"
-	run_at_finish = "None"
-	save_comments = "true"
-	log_to_file = "false"
-	custom_config_path = 'pyinstalive.ini'
-	use_locks = "false"

+ 240 - 0
pyinstalive/startup.py

@@ -0,0 +1,240 @@
+import argparse
+import configparser
+import os
+import logging
+import platform
+import subprocess
+
+try:
+    import pil
+    import auth
+    import logger
+    import helpers
+    import downloader
+    import assembler
+    from constants import Constants
+except ImportError:
+    from . import pil
+    from . import auth
+    from . import logger
+    from . import helpers
+    from . import downloader
+    from . import assembler
+    from .constants import Constants
+
+
+def validate_inputs(config, args, unknown_args):
+    error_arr = []
+    try:
+        config.read(pil.config_path)
+
+        if helpers.bool_str_parse(config.get('pyinstalive', 'log_to_file')) == "Invalid":
+            pil.log_to_file = False
+            error_arr.append(['log_to_file', 'False'])
+        elif helpers.bool_str_parse(config.get('pyinstalive', 'log_to_file')):
+            pil.log_to_file = True
+        else:
+            pil.log_to_file = False
+
+        logger.banner()
+
+        if unknown_args:
+            pil.uargs = unknown_args
+            logger.warn("The following unknown argument(s) were provided and will be ignored: ")
+            logger.warn('    ' + ' '.join(unknown_args))
+            logger.separator()
+
+        pil.ig_user = config.get('pyinstalive', 'username')
+        pil.ig_pass = config.get('pyinstalive', 'password')
+        pil.dl_path = config.get('pyinstalive', 'download_path')
+        pil.run_at_start = config.get('pyinstalive', 'run_at_start')
+        pil.run_at_finish = config.get('pyinstalive', 'run_at_finish')
+        pil.ffmpeg_path = config.get('pyinstalive', 'ffmpeg_path')
+        pil.args = args
+        pil.config = config
+
+        if args.configpath:
+            pil.config_path = args.configpath
+            if not os.path.isfile(pil.config_path):
+                pil.config_path = os.path.join(os.getcwd(), "pyinstalive.ini")
+                logger.warn("Custom config path is invalid, falling back to default path: {:s}".format(pil.config_path))
+                logger.separator()
+
+        if args.dlpath:
+            pil.dl_path = args.dlpath
+
+        if helpers.bool_str_parse(config.get('pyinstalive', 'show_cookie_expiry')) == "Invalid":
+            pil.show_cookie_expiry = False
+            error_arr.append(['show_cookie_expiry', 'False'])
+        elif helpers.bool_str_parse(config.get('pyinstalive', 'show_cookie_expiry')):
+            pil.show_cookie_expiry = True
+        else:
+            pil.show_cookie_expiry = False
+
+        if helpers.bool_str_parse(config.get('pyinstalive', 'use_locks')) == "Invalid":
+            pil.use_locks = False
+            error_arr.append(['use_locks', 'False'])
+        elif helpers.bool_str_parse(config.get('pyinstalive', 'use_locks')):
+            pil.use_locks = True
+        else:
+            pil.use_locks = False
+
+        if helpers.bool_str_parse(config.get('pyinstalive', 'clear_temp_files')) == "Invalid":
+            pil.clear_temp_files = False
+            error_arr.append(['clear_temp_files', 'False'])
+        elif helpers.bool_str_parse(config.get('pyinstalive', 'clear_temp_files')):
+            pil.clear_temp_files = True
+        else:
+            pil.clear_temp_files = False
+
+        if not args.nolives and helpers.bool_str_parse(config.get('pyinstalive', 'download_lives')) == "Invalid":
+            pil.dl_lives = True
+            error_arr.append(['download_lives', 'True'])
+        elif helpers.bool_str_parse(config.get('pyinstalive', 'download_lives')):
+            pil.dl_lives = True
+        else:
+            pil.dl_lives = False
+
+        if not args.noreplays and helpers.bool_str_parse(config.get('pyinstalive', 'download_replays')) == "Invalid":
+            pil.dl_replays = True
+            error_arr.append(['download_replays', 'True'])
+        elif helpers.bool_str_parse(config.get('pyinstalive', 'download_replays')):
+            pil.dl_replays = True
+        else:
+            pil.dl_replays = False
+
+        if helpers.bool_str_parse(config.get('pyinstalive', 'download_comments')) == "Invalid":
+            pil.dl_comments = True
+            error_arr.append(['download_comments', 'True'])
+        elif helpers.bool_str_parse(config.get('pyinstalive', 'download_comments')):
+            pil.dl_comments = True
+        else:
+            pil.dl_comments = False
+
+        if pil.ffmpeg_path:
+            if not os.path.isfile(pil.ffmpeg_path):
+                pil.ffmpeg_path = None
+                cmd = "where" if platform.system() == "Windows" else "which"
+                logger.warn("Custom ffmpeg binary path is invalid, falling back to default path: {:s}".format(
+                    subprocess.check_output([cmd, 'ffmpeg']).decode('UTF-8').rstrip()))
+            else:
+                logger.binfo("Overriding ffmpeg binary path: {:s}".format(pil.ffmpeg_path))
+
+        if not pil.ig_user or not len(pil.ig_user):
+            raise Exception("Invalid value for 'username'. This value is required.")
+
+        if not pil.ig_pass or not len(pil.ig_pass):
+            raise Exception("Invalid value for 'password'. This value is required.")
+
+        if not pil.dl_path.endswith('/'):
+            pil.dl_path = pil.dl_path + '/'
+        if not pil.dl_path or not os.path.exists(pil.dl_path):
+            pil.dl_path = os.getcwd()
+            if not args.dlpath:
+                error_arr.append(['download_path', os.getcwd()])
+            else:
+                logger.warn("Custom config path is invalid, falling back to default path: {:s}".format(pil.dl_path))
+                logger.separator()
+
+        if args.nolives:
+            pil.dl_lives = False
+
+        if args.noreplays:
+            pil.dl_replays = False
+
+        if error_arr:
+            for error in error_arr:
+                logger.warn("Invalid value for '{:s}'. Using default value: {:s}".format(error[0], error[1]))
+                logger.separator()
+
+        if args.download:
+            pil.dl_user = args.download
+        elif not args.clean and not args.info and not args.assemble and not args.downloadfollowing:
+            logger.error("Missing --download argument. This argument is required.")
+            logger.separator()
+            return False
+
+        if args.info:
+            helpers.show_info()
+            return False
+        elif args.clean:
+            helpers.clean_download_dir()
+            return False
+        elif args.assemble:
+            pil.assemble_arg = args.assemble
+            assembler.assemble()
+            return False
+
+        return True
+    except Exception as e:
+        logger.error("An error occurred: {:s}".format(str(e)))
+        logger.error("Make sure the config file and given arguments are valid and try again.")
+        logger.separator()
+        return False
+
+
+def run():
+    pil.initialize()
+    logging.disable(logging.CRITICAL)
+    config = configparser.ConfigParser()
+    parser = argparse.ArgumentParser(
+        description="You are running PyInstaLive {:s} using Python {:s}".format(Constants.SCRIPT_VER,
+                                                                                Constants.PYTHON_VER))
+
+    parser.add_argument('-u', '--username', dest='username', type=str, required=False,
+                        help="Instagram username to login with.")
+    parser.add_argument('-p', '--password', dest='password', type=str, required=False,
+                        help="Instagram password to login with.")
+    parser.add_argument('-d', '--download', dest='download', type=str, required=False,
+                        help="The username of the user whose livestream or replay you want to save.")
+    parser.add_argument('-i', '--info', dest='info', action='store_true', help="View information about PyInstaLive.")
+    parser.add_argument('-nr', '--no-replays', dest='noreplays', action='store_true',
+                        help="When used, do not check for any available replays.")
+    parser.add_argument('-nl', '--no-lives', dest='nolives', action='store_true',
+                        help="When used, do not check for any available livestreams.")
+    parser.add_argument('-cl', '--clean', dest='clean', action='store_true',
+                        help="PyInstaLive will clean the current download folder of all leftover files.")
+    parser.add_argument('-cp', '--config-path', dest='configpath', type=str, required=False,
+                        help="Path to a PyInstaLive configuration file.")
+    parser.add_argument('-dp', '--download-path', dest='dlpath', type=str, required=False,
+                        help="Path to folder where PyInstaLive should save livestreams and replays.")
+    parser.add_argument('-as', '--assemble', dest='assemble', type=str, required=False,
+                        help="Path to json file required by the assembler to generate a video file from the segments.")
+    parser.add_argument('-df', '--download-following', dest='downloadfollowing', action='store_true',
+                        help="PyInstaLive will check for available livestreams and replays from users the account "
+                             "used to login follows.")
+
+    # Workaround to 'disable' argument abbreviations
+    parser.add_argument('--usernamx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('--passworx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('--infx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('--noreplayx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('--cleax', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('--downloadfollowinx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('--configpatx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('--confix', help=argparse.SUPPRESS, metavar='IGNORE')
+
+    parser.add_argument('-cx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('-nx', help=argparse.SUPPRESS, metavar='IGNORE')
+    parser.add_argument('-dx', help=argparse.SUPPRESS, metavar='IGNORE')
+
+    args, unknown_args = parser.parse_known_args()  # Parse arguments
+
+    if not os.path.exists(pil.config_path):  # Create new config if it doesn't exist
+        logger.banner()
+        helpers.new_config()
+        return
+
+    if validate_inputs(config, args, unknown_args):
+        if not args.username and not args.password:
+            pil.ig_api = auth.authenticate(username=pil.ig_user, password=pil.ig_pass)
+        elif (args.username and not args.password) or (args.password and not args.username):
+            logger.warn("Missing --username or --password argument. Falling back to config file.")
+            logger.separator()
+            pil.ig_api = auth.authenticate(username=pil.ig_user, password=pil.ig_pass)
+        elif args.username and args.password:
+            pil.ig_api = auth.authenticate(username=args.username, password=args.password, force_use_login_args=True)
+
+        if pil.ig_api:
+            downloader.start()
+