var os = require("os"),
fs = require("fs"),
path = require("path"),
url = require("url"),
http = require("http"),
https = require("https"),
crypto = require("crypto"),
zlib = require("zlib"),
//tls = require("tls"),
//ciphers_default = tls.DEFAULT_CIPHERS.split(":"),
//ciphers_shuffled = [ // https://httptoolkit.tech/blog/tls-fingerprinting-node-js/
//ciphers_default[2],
//ciphers_default[1],
//ciphers_default[0],
//...ciphers_default.slice(3)
//].join(":"),
child_process = require("child_process"),

cookie = "",
//cookie = "sid_tt=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",

cookie_web_api = [
//	t=uvwp
	"s_v_web_id=verify_a2a4659cf13109c13654ee74373934fe",
//	t=uvw
	"msToken=vttSC0V0WY3esXdP8IbyumsvaPwtTVxDdUriGikfR5BKkjBpqAiRVhLiZ1xclv2Y40wrmvbfcH62rixaX5ujon5F2P8sVujfGykErkEiuFZBFMPYVvCqPDCY7qpQ91NXQc_ViQPX6MKei0dfrA=="
].join("; "),

reject_sd = true,
reject_sd_error = new Error("SD"),
uri_blocks = new Set(),

user_agent = "com.zhiliaoapp.musically/2022400030 (Linux; U; Android 7.1.2; en_US; SM-G973N; Build/PPR1.190810.011;tt-ok/3.10.0.2)",

//user_agents,
//user_agents_random = false,
user_agents_random = true,

//cycle_hosts = false,
cycle_hosts = true,

download_videos_tikwm = false,
//download_videos_tikwm = true,

download_videos_single = false,
//download_videos_single = true,

magic_bytes = "66747970",
magic_bytes_start = 4,
magic_bytes_end = 7,
//test_magic_bytes = false,
test_magic_bytes = true,

windows_max_path = 255,
//windows_max_path = 260,
windows_paths = false,
//windows_paths = process.platform === "win32",

debug = false,
//debug = true,

debug_data = false,
//debug_data = true,

help = "node " + path.basename(__filename) + " t=ui|mi|hi|vi|uv|mv|hv|v|us|s|c|cm|cv|vc|vcr|ul|fyp|followed|uvw|uvwp si=sec_uid|i=0|@name|#tag|clipboard|\"file.txt\" \\"
+ os.EOL + "[l=-1|" + (new Date(0)).toISOString().substring(0, 10) + "|last_scrape] [last_scrape] [y] \\"
+ os.EOL + "[o=0] [c] [d=-1] [q] [h] [f] [p] [a] [b] [z] [ui] [un] \\"
+ os.EOL + "[g=\"log.txt\"] [log_errors] [log_verbose] [debug] [debug_data] \\"
+ os.EOL + "[w=30] [x] [n=100] [error_delay=1000] \\"
+ os.EOL + "[cookie=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] = [j=\"sid_tt=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; sessionid=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"] \\"
+ os.EOL + "[u=\"" + user_agent + "\"] [ur] \\"
+ os.EOL + "[m=\"metadata_file.json\"] [v] [s] [k] [metadata_each] [metadata_dirs] \\"
+ os.EOL + "[fetch_only] [fetched_ids=\"file.json\"] [scraped_ids=\"file.json\"] [unscraped_ids=\"file.json\"] [ignore_ids=\"file.json\"] [save_empty] \\"
+ os.EOL + "[max_depth=0] [max_resumes=10] [max_retries=3] [max_converts=1] [max_downloads=10] [max_zips=3] \\"
+ os.EOL + "[min_views=0] [max_views=0] \\"
+ os.EOL + "[skip_photos] [skip_stories] [skip_followed] [skip_uids=\"skip_uids.txt\"] \\"
+ os.EOL + "[only_photos] [only_stories] [only_followed] [only_uids=\"only_uids.txt\"] \\"
+ os.EOL + "[zip_photos] [story_dirs] [tag_mutual] [tag_stories] \\"
+ os.EOL + "[aweme_details_single] [aweme_host=\"*.tiktokv.com\"] [cycle_hosts] \\"
+ os.EOL + "[verify_files] [rename_hevc] [convert_hevc] \\"
+ os.EOL + "[alert_proc] [alert_args={}] [progress_bars] [windows_paths=" + windows_paths + "] [tikwm] [aid=1233]"
+ os.EOL
+ os.EOL + "t - ui (user info), mi (music info), hi (hashtag info), vi (video info),"
+ os.EOL + "    uv (user videos), mv (music videos), hv (hashtag videos), v (videos),"
+ os.EOL + "    us (user stories), s ([tiktok's selection of followed accounts]' stories),"
+ os.EOL + "    c (personal collections' videos), cm (personal collections' metadata), cv (collection's videos),"
+ os.EOL + "    vc (video comments), vcr (video comment's replies), ul (user likes),"
+ os.EOL + "    fyp (trending videos), followed (followed accounts' videos),"
+ os.EOL + "    uvw (user videos [web api]), uvwp (user videos [web page])"
+ os.EOL + "i - id or @name of a user, id of a music, id or #tag of a hashtag,"
+ os.EOL + "    id of a video, or empty value for stdin, or file or clipboard with ids or urls of videos (separated by whitespace)"
+ os.EOL + "l - date until which to check videos (YYYY-MM-DD),"
+ os.EOL + "    last_scrape to continue until encountering a video created <= the previous use,"
+ os.EOL + "    or max number of videos to check (integer)"
+ os.EOL + "o - cursor (integer) - unix time in milliseconds (user videos) or video offset (music and hashtag videos)"
+ os.EOL + "c - add date"
+ os.EOL + "d - add description - pass a number to limit to that file name size by trimming descriptions"
+ os.EOL + "q - download videos in hd"
+ os.EOL + " =0 - download videos in sd, tagged"
+ os.EOL + " =1 - download videos in sd, tagged, and hd"
+ os.EOL + " =2 - download videos in sd, tagged, if downloading in hd fails"
+ os.EOL + " =3 - download videos in sd (with watermark), tagged"
+ os.EOL + " =4 - download videos in sd, tagged, sd (with watermark), tagged, and hd"
+ os.EOL + " =-1 - download videos in sd (with watermark)"
+ os.EOL + "h - prefer HEVC/H.265"
+ os.EOL + "g - log output to file"
+ os.EOL + "f - directory"
+ os.EOL + "p - don't download videos/photos"
+ os.EOL + "a - download thumbnails"
+ os.EOL + "b - download animated thumbnails"
+ os.EOL + "z - group files in sub-directories by creator uid"
+ os.EOL + "k - log metadata"
+ os.EOL + "m - save metadata to file - if not passed an output file then uses a default construction"
+ os.EOL + "s - split metadata results by newline"
+ os.EOL + "v - unfiltered metadata"
+ os.EOL + "w - cursor batch size"
+ os.EOL + "x - log current cursor - if passed a value then also shows time elapsed between cursors"
+ os.EOL + "y - override the limit set by the \"l\" flag to finish the final cursor's batch"
+ os.EOL + "n - re-attempt requests on server error - takes a max amount of errors before a success to ignore (0 = no limit)"
+ os.EOL + "j - private cookie from \".tiktok.com\""
+ os.EOL + "u - user agent"
+ os.EOL + "ur - randomized user agents"
+ os.EOL + "ui - add user id"
+ os.EOL + "un - add user name"
+ os.EOL
+ os.EOL + "verify_files  ^ download videos as (%file_name%~verify.mp4),"
+ os.EOL + "                pass through ffprobe after downloading to check their integrity,"
+ os.EOL + "                if failure then rename (%file_name%~error.mp4)"
+ os.EOL + "                else rename according to other flags set"
+ os.EOL + "rename_hevc   ^ if downloaded video is HEVC then rename (%file_name%~hevc.mp4)"
+ os.EOL + "convert_hevc  & if downloaded video is HEVC then convert a H264 version (%file_name%~h264.mp4)"
+ os.EOL + "              ^ (requires ffprobe)"
+ os.EOL + "              & (requires ffprobe+ffmpeg)"
+ os.EOL
+ os.EOL + "metadata_each - save separate metadata files for each video,"
+ os.EOL + "                if passed a value then also updates previously saved metadata"
+ os.EOL + "metadata_dirs - save separate metadata files in a sub-directory,"
+ os.EOL + "                defaults to /metadata but also accepts another value to use"
+ os.EOL
+ os.EOL + "max_depth     - max amount of cursors to scrape through"
+ os.EOL + "max_resumes   - max amount of times to attempt resuming a timed-out download"
+ os.EOL + "max_retries   - max amount of times to attempt re-downloading a video after it fails ffprobe"
+ os.EOL + "max_converts  - max amount of simultaneous file converts"
+ os.EOL + "max_downloads - max amount of simultaneous downloads"
+ os.EOL + "max_zips      - max amount of simultaneous photo zip operations"
+ os.EOL
+ os.EOL + "fetch_only    - only fetch ids, skip downloading files/metadata"
+ os.EOL + "fetched_ids   - save fetched ids to file"
+ os.EOL + "scraped_ids   - save fetched and scraped ids to file"
+ os.EOL + "unscraped_ids - save fetched but unscraped ids to file"
+ os.EOL + "ignore_ids    - ignore ids already present in supplied file,"
+ os.EOL + "                add successfully downloaded ids to same file"
+ os.EOL + "              - if not passed output files then uses default constructions"
+ os.EOL + "save_empty    - save output files to disk even if the contents are empty"
+ os.EOL
+ os.EOL + "alert_proc    - process to run newly downloaded files through"
+ os.EOL + "alert_args    - arguments to pass to the process"
+ os.EOL + "                \"{}\" is replaced with the downloaded file's path"
+ os.EOL
+ os.EOL + "debug         - show http requests and file operations"
+ os.EOL + "debug_data    - show http responses"
+ os.EOL + "log_errors    - write errors to the log file"
+ os.EOL + "log_verbose   - write all non error output to the log file"
+ os.EOL
+ os.EOL + "aid           - app id"
+ os.EOL + "tikwm         - download videos directly from tikwm without using the details api"
+ os.EOL + "cookie        - content of the sid_tt/sessionid cookie"
+ os.EOL + "zip_photos    % zip photo posts after download"
+ os.EOL + "story_dirs    - directory to save story posts to"
+ os.EOL + "tag_mutual    - tag mutual only posts"
+ os.EOL + "tag_stories   - tag story posts"
+ os.EOL + "error_delay   - interval between re-attempts with flag \"n\" (ms)"
+ os.EOL + "last_scrape   - save the creation timestamp of the most recent post, for use with \"l=last_scrape\""
+ os.EOL + "strip_spaces  - consolidate/trim whitespace in filename descriptions"
+ os.EOL + "windows_paths - remove descriptions if resulting file path would be >= " + windows_max_path + " characters (windows max safe file path)"
+ os.EOL + "progress_bars $ visual indicator for files' download progress"
+ os.EOL + "                if passed a value then also shows line count"
+ os.EOL + "              % (requires archiver)"
+ os.EOL + "              $ (requires multi-progress and cli-cursor)"
+ os.EOL
+ os.EOL + "min_views     - filter out posts with fewer views"
+ os.EOL + "max_views     - filter out posts with more views"
+ os.EOL
+ os.EOL + "skip_photos   - filter out photo posts"
+ os.EOL + "skip_stories  - filter out story posts"
+ os.EOL + "skip_followed - filter out posts created by followed users"
+ os.EOL + "skip_uids     - filter out posts created by given users"
+ os.EOL
+ os.EOL + "only_photos   - filter out non photo posts"
+ os.EOL + "only_stories  - filter out non story posts"
+ os.EOL + "only_followed - filter out posts not created by followed users"
+ os.EOL + "only_uids     - filter out posts not created by given users"
+ os.EOL
+ os.EOL + "aweme_details_single   - use alternate details endpoint"
+ os.EOL + "aweme_host             - host to attempt first for api /play calls"
+ os.EOL + "aweme_host_posts       -"
+ os.EOL + "aweme_host_details     -"
+ os.EOL + "aweme_host_likes       -"
+ os.EOL + "aweme_host_music       -"
+ os.EOL + "aweme_host_tags        -"
+ os.EOL + "aweme_host_stories     -"
+ os.EOL + "aweme_host_comments    -"
+ os.EOL + "aweme_host_collections -"
+ os.EOL + "aweme_host_trending    -"
+ os.EOL + "aweme_host_followed    - hosts to use for api calls"
+ os.EOL + "cycle_hosts            - rotate through hosts on error"
+ os.EOL
+ os.EOL + "node " + path.basename(__filename) + " e=file.txt|[cmd f] [r=-1] [p=1] [o] [s] [i=clipboard|uid|uids.txt|metadata_file.json]"
+ os.EOL + "e - file (empty value for stdin) with commands (separated by newline,"
+ os.EOL + "    separate arguments with space,"
+ os.EOL + "    escape arguments with quotation marks or apostrophes"
+ os.EOL + "    and quotation marks and apostrophes by doubling)"
+ os.EOL + "    comment out lines with #"
+ os.EOL + "f - treat flag \"e\" as a single command instead of as a file to read"
+ os.EOL + "d - write given commands to file, after any expansion"
+ os.EOL + "r - repeat interval in seconds"
+ os.EOL + "p - max concurrent processes"
+ os.EOL + "o - redirect output to stdout and stderr"
+ os.EOL + "s - run commands through the shell"
+ os.EOL + "u - rebuild commands from given file(s) on restarting its loop"
+ os.EOL + "i - uid, or file or clipboard with list of uids to insert into the commands in place of the string \"{}\"",
sec_uid = false,
limit = -1,
cursor = 0,
//count = 30,
count = 50,
date_limit = false,
date = false,
description = false,
description_limit = -1,
//description_limit = 200,
//description_limit = 255,
quality = false,
hevc = false,
previous_scrape = 0,
last_scrape = 0,
last_scrape_save = false,
last_scrape_file,
last_scrapes_dir = "last_scrapes",
log_save = false,
log_errors = false,
log_verbose = false,
log_file = "log.txt",
logger,
cursor_print = false,
cursor_times = false,
break_limit = false,
ignore_errors = false,
//ignore_errors_delay = 1000,
ignore_errors_delay = 0,
ignore_errors_limit = 100,
ignore_errors_count = 0,
uid_directories = false,
directories_created = new Set(),
aid = 1233,
fetch_only = false,
download_videos = true,
download_thumbs = false,
download_thumbs_anim = false,
zip_photos = false,
archiver,
download_files = true,
request_aweme_cache = true,
request_aweme_cache_dir = "request_aweme_cache",
request_aweme_static = false,
request_aweme_static_dir = "static_uris",
metadata = [],
metadata_verbose = false,
metadata_split = false,
metadata_print = false,
metadata_save = false,
metadata_save_ignored = false,
//metadata_save_ignored = true,
metadata_save_each = false,
metadata_update_each = false,
metadata_dirs = false,
metadata_dir_name = "metadata",
metadata_file,
story_dirs = false,
story_dir_name = "stories",
max_depth = 0,
max_resumes = 10,
max_retries = 3,
max_converts = 1,
max_downloads = 10,
max_zips = 3,
queued_zips = [],
active_zips = 0,
fetched_ids = [],
fetched_ids_save = false,
fetched_ids_file,
scraped_ids = [],
scraped_ids_save = false,
scraped_ids_file,
unscraped_ids = [],
unscraped_ids_save = false,
unscraped_ids_file,
ignore_ids = new Set(),
ignore_ids_initial = new Set(),
ignore_ids_save = false,
ignore_ids_file,
ignore_ids_dir = "ignore_ids",
save_empty = false,
skip_min_views = false,
skip_max_views = false,
skip_photos = false,
skip_stories = false,
skip_followed = false,
skip_uids = false,
skip_uids_file = "skip_uids.txt",
only_photos = false,
only_stories = false,
only_followed = false,
only_uids = false,
only_uids_file = "only_uids.txt",
file_tag_delimiter = "~",
file_tag_default = "",
file_tag_wm = "wm",
file_tag_sd = "sd",
file_tag_verify = "verify",
//it's important that this tag has >= the characters of the other tags,
//there could be a valid file name with a short tag that would be overlong with a longer one
file_tag_hevc = "hevc",
file_tag_h264 = "h264",
file_tag_error = "error",
file_tag_mutual = "mutual",
file_tag_stories = "story",
strip_spaces = false,
tag_uid = false,
tag_unique_id = false,
tag_mutual = false,
tag_stories = false,
tag_wm = true,
tag_sd = false,
download_wm = false,
download_sd = true,
download_hd = false,
fallback_sd = false,
verify_files = false,
rename_hevc = false,
convert_hevc = false,
queued_converts = [],
active_converts = 0,
require_ffprobe = false,
cached_content_lengths = {},
ffmpeg,
ffprobe,
ffmpeg_path,
ffprobe_path,
//ffmpeg_path = "ffmpeg",
//ffprobe_path = "ffprobe",
alert = false,
alert_proc,
alert_args = "{}",
progress_bars = false,
//progress_bars_stream = process.stderr,
progress_bars_stream = process.stdout,
progress_bars_debug = false,
multi_progress,
output_tags = "tiloy",
output_time = Date.now(),
//root_directory = process.cwd(),
root_directory = __dirname,
directory = root_directory + path.sep + "download",
running = true,
timeout = null,
commands_file,
commands_file_cmds,
commands_file_uids,
commands_file_func,
idRegex = /^[1-9][0-9]{0,19}$/,
uidRegex = /^@?[1-9][0-9]{0,19}$/,
aweme_hosts = ["api16-normal-c-useast2a.tiktokv.com","api19-normal-c-useast2a.tiktokv.com","api21-normal-c-useast2a.tiktokv.com","api22-normal-c-useast2a.tiktokv.com"],
aweme_host_posts = "api16-core-c-useast2a.tiktokv.com",
aweme_host_details = "api16-normal-c-useast2a.tiktokv.com",
aweme_host_likes = aweme_host_details,
aweme_host_music = aweme_host_details,
aweme_host_tags = aweme_host_details,
aweme_host_stories = aweme_host_details,
aweme_host_comments = aweme_host_details,
aweme_host_collections = aweme_host_details,
aweme_host_trending = aweme_host_details,
aweme_host_followed = aweme_host_details,
deviceId = generateDeviceId(),
//iId = generateDeviceId(),
deviceVersion = generateBase62(8);
function generateBase62(length) {
	var i = 0, r = "", base62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	while (i < length) {
		r += base62.charAt(Math.floor(Math.random() * 62));
		i++;
	}
	return r;
}
function generateDeviceId() {
	var device_ids = [
		"6006859028954454735", "6012800195775468511",
		"6034970553342014751", "6034988560490741270",
		"6039670484042628740", "6046195507000192112",
		"6050884135905976003", "6055737945486455208",
		"6092265139012270405", "6094698334097707653",
		"6108431223504603930", "6122854733252896327",
		"6132638236601811401", "6143329866930436055",
		"6145176890584062721", "6148112242033170517",
		"6149245134388144003", "6149906059270871956",
		"6157450911109482461", "6161578061150404600",
		"6168275655010891506", "6170902093952376320",
		"6179483841612854782", "6182491179034723839",
		"6215783218609401201", "6218897961737681514",
		"6229103192129950300", "6252302298359660242",
		"6259587017792698335", "6284634047155796980",
		"6288470714824626994", "6290631856470802522",
		"6291313789597379872", "6303690201251033480",
		"6320505096619586697", "6328026909420163450",
		"6335389761841004210", "6335637948229882619",
		"6352384946536385669", "6356044788456090156",
		"6360970049774249485", "6364235424205091002",
		"6365886134451466967", "6366833378980346894",
		"6370754609932077430", "6378113895478834314",
		"6389604209911809222", "6390462282151036358",
		"6395454122262736803", "6396307455307352061",
		"6401866201397954359", "6406277069274400697",
		"6409402101798356405", "6425150406654712096",
		"6434047360308840860", "6457507857077512738",
		"6462224924713941883", "6463440317663095861",
		"6465171677952415292", "6466394902948059476",
		"6472325003823007761", "6480536383995438813",
		"6487734595550019103", "6489508510469169994",
		"6493329967311166354", "6494887258884743841",
		"6496637042788692703", "6501095260708613530",
		"6501155853504052486", "6503937539203199465",
		"6505523166794951057", "6506916210460139784",
		"6521325171758460202", "6525885027548887050",
		"6535567861480630803", "6538139048978832398",
		"6557944438726343636", "6558543576312108465",
		"6570543418341780332", "6576742432282773570",
		"6598442881814793212", "6602379449676645302",
		"6602658211094454602", "6611914144945180750",
		"6612354682437219625", "6628735378226681288",
		"6629849374300942250", "6630301239078106874",
		"6639949969251392766", "6651067794263341822",
		"6654522973774363689", "6656138273631798555",
		"6667798506097974982", "6687507287926276172",
		"6689378219089026538", "6708149516213856967",
		"6710943047421341123", "6723516052420499469",
		"6738801530028650207", "6756994174775326212",
		"6758130111665881327", "6758862040713780141",
		"6790632202118168881", "6791810888267928078",
		"6794790519618876283", "6794986289308293231",
		"6797066007407300802", "6800032337930565941",
		"6837358400440284080", "6840383908283926022",
		"6845813237382669834", "6853862914715035283",
		"6865800658848031451", "6874654938079359762",
		"6883605107865071584", "6901755573090213356",
		"6911644609473478931", "6918274087750866720",
		"6932554083332094140", "6933591380512842592",
		"6936151354448753102", "6937501352823782580",
		"6943615465151344926", "6945616506637819449",
		"6951181942413090594", "6962901784826151388",
		"6970401811297761583", "6973728867057423193"
	];
	return device_ids[Math.floor(Math.random() * device_ids.length)];
}
//function generateDeviceId() {
//	var i = 1, a = new Array(19);//Math.floor(Math.random() * 9) + 11
//	a[0] = 54;//Math.floor(Math.random() * 9) + 49
//	while (i < a.length) {
//		a[i++] = Math.floor(Math.random() * 10) + 48;
//	}
//	return String.fromCharCode.apply(null, a);
//}
function generateVerify() {
	var i = 0, d = new Date(), t = d.getTime() - d.getTimezoneOffset() * 60000, r = "", base62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
	do {
		switch (i) {
			case 8:
			case 13:
			case 18:
			case 23:
				r += "_";
				break;
			case 14:
				r += "4";
				break;
			default:
				r += base62.charAt(Math.floor(Math.random() * 62));
				break;
		}
		i++;
	} while (i < 36);
	return "verify_" + t.toString(36) + "_" + r;
}
function generateCookie() {
	var t = generateDeviceId();
	return "sessionid=; tt_webid=" + t + "; tt_webid_v2=" + t + "; tt_csrf_token=" + generateBase62(24) + "; s_v_web_id=" + generateVerify();
}
function getQualityUris(i, host) {
	return [host,...aweme_hosts].map(h => "https://" + h + "/aweme/v1/play/?video_id=" + i + "&line=1&improve_bitrate=1&ratio=default");
//	return aweme_hosts.map(h => "https://" + h + "/aweme/v1/play/?video_id=" + i + "&line=1&improve_bitrate=1&ratio=default");
}
function getWatermarkedUriApi(aweme_id) {
	return "https://tikwm.com/video/media/wmplay/" + aweme_id + ".mp4";
}
function getUriApi(aweme_id) {
	return "https://tikwm.com/video/media/play/" + aweme_id + ".mp4";
}
function getQualityUriApi(aweme_id) {
//	return Array.from(Array(max_retries)).map(() => "https://tikwm.com/video/media/hdplay/" + aweme_id + ".mp4");
	return ["https://tikwm.com/video/media/hdplay/" + aweme_id + ".mp4"];
}
//function getQualityUri(value) {
//	var u = new URL(value), i = u.searchParams.get("video_id");
//	if (i !== null) {
//		return getQualityUris(i, u["host"]);
//	}
//	return [value];
//}
function getUri(i, host) {
	return "https://" + host + "/aweme/v1/play/?video_id=" + i;
}
function createTimeToString(value) {
	var t = (new Date(value * 1000)).toISOString();
	return t.length === 24 ? t.substring(0, 10) + " " + t.substring(11, 19) : t.substring(0, 13) + " " + t.substring(14, 22);
}
function createTimeToFilename(value) {
	var t = (new Date(value * 1000)).toISOString();
	return t.substring(0, t.length === 24 ? 19 : 22).replace(/[-:]/g, "") + "Z";
}
function createTimeToLog(value) {
	var t = (new Date(value * 1000)).toISOString();
	return (t.length === 24 ? t.substring(0, 19) : t.substring(0, 22)) + "Z";
}
function sanitizeFilename(value) {
	var v = value.replace(/[\x00-\x1f"*\/:<>?\\|]/g, "");
	return strip_spaces ? v.replace(/\s+/g, " ").trim() : v;
}
function sanitizeLog(value) {
	return value.replace(/[\r\n]/g, "");
}
function ensureFileDirectorySync(file, callback, log_debug=true) {
	if (debug && log_debug) console_log("check_file_directory: " + file);
	if (!directories_created.has(directory) && !path.isAbsolute(file)) {
		try {
			if (debug && log_debug) console_log("create_directory: " + directory);
			ensureDirectorySync(directory);
			if (debug && log_debug) console_log("change_directory: " + directory);
			process.chdir(directory);
			directories_created.add(directory);
		} catch (e) {
			if (callback !== undefined) {
				callback(e, file);
			} else {
				console_error(e);
			}
			return;
		}
	}
	var file_directory = path.dirname(file);
	if (!directories_created.has(file_directory)) {
		try {
			if (debug && log_debug) console_log("create_directory: " + file_directory);
			ensureDirectorySync(file_directory);
			directories_created.add(file_directory);
		} catch (e) {
			if (callback !== undefined) {
				callback(e);
			} else {
				console_error(e);
			}
			return;
		}
	}
	return null;
}
function ensureDirectorySync(directory) {
	if ((directory = path.normalize(directory)).lastIndexOf(path.sep) + path.sep.length >= directory.length) {
		directory = directory.substring(0, directory.length - path.sep.length);
	}
	try {
		var dir_stats = fs.lstatSync(directory);
		if (dir_stats.isSymbolicLink()) {
			directory = fs.readlinkSync(directory);
			dir_stats = fs.lstatSync(directory);
		}
		if (!dir_stats.isDirectory()) {
			throw new Error("Path is not a directory.");
		}
	} catch (e) {
		if (e.code !== "ENOENT") {
			throw e;
		}
		for (var i = directory.length + path.sep.length, e, t; (i -= path.sep.length) > -1
		&& (i = directory.lastIndexOf(path.sep, i)) > -1
		&& !fs.existsSync(t = directory.substring(0, i));) {}
		if (i > -1) {
			i += path.sep.length;
			e = t.length;
		} else if (directory.substring(0, path.sep.length) === path.sep) {
			t = "";
			i = path.sep.length;
			e = 0;
		} else {//root
			t = "." + path.sep;
			i = 0;
			e = 0;
		}
		for (; (i = directory.indexOf(path.sep, i)) > -1; e = i, i += path.sep.length) {
			fs.mkdirSync(t += directory.substring(e, i));
		}
		fs.mkdirSync(t + directory.substring(e));
	}
}
function emptyFunction() {}
function parseArgv(arguments) {
	var i = 2, e, r = {};
	while (i < arguments.length) {
		e = arguments[i].indexOf("=");
		if (e === -1) {
			r[arguments[i]] = "";
		} else {
			r[arguments[i].substring(0, e)] = arguments[i].substring(e + 1);
		}
		i++;
	}
	return r;
}
function readStdIn(callback) {
	var d = "", s = process.stdin.isTTY ? fs.createReadStream(null, {fd: process.stdin.fd}) : process.stdin;
	s.setEncoding("utf8");
	s.on("data", function(data) {
		d += data;
	}).on("error", function(error) {
		if (callback !== null) {
			callback(error);
			callback = null;
		}
	}).on("end", function() {
		if (callback !== null) {
			callback(null, d);
			callback = null;
		}
	});
	s.resume();
}
function getCmds(value) {
	var i = 0, lr = /(?:\r\n|[\r\n])+/g, m, r = [];
	while ((m = lr.exec(value)) !== null) {
		if (i < lr.lastIndex - m[0].length) {
			r.push(value.substring(i, lr.lastIndex - m[0].length));
		}
		i = lr.lastIndex;
	}
	if (i < value.length) {
		r.push(value.substring(i));
	}
	var cmds = r.filter(v => !v.match(/^\s*#/));
	if (commands_file_uids !== undefined && commands_file_uids.length > 0) {
		var uids = [];
		if (uidRegex.test(commands_file_uids)) {
			uids = [commands_file_uids.replace(/^@/, "")];
		} else {
			try {
				if (commands_file_uids === "clipboard") {
					uids = (require("clipboardy")).readSync();
				} else if (path.extname(commands_file_uids) === ".json") {
					uids = JSON.parse(fs.readFileSync(file_path_absolute(commands_file_uids, root_directory), {flag: "r", encoding: "utf8"}));
				} else {
					uids = fs.readFileSync(file_path_absolute(commands_file_uids, root_directory), {flag: "r", encoding: "utf8"});
				}
				uids = [...new Set(commands_file_func(uids))];
			} catch (e) {
				uids = [];
				console_error(e);
			}
		}
//		if (uids !== undefined && uids.length > 0) {
			var cmds_tmp = [];
			for (var j = 0; j < cmds.length; j++) {
				if (cmds[j].includes("{}")) {
					for (var k = 0; k < uids.length; k++) {
						cmds_tmp.push(cmds[j].replaceAll("{}", uids[k]));
					}
				} else {
					cmds_tmp.push(cmds[j]);
				}
			}
			cmds = cmds_tmp;
//		}
	}
	return cmds;
}
function getIdsJson(ks) {
	return ks.reduce(function(v, k) {
		if (typeof k.aweme_id === "string") {
			v.push(k.aweme_id);
		}
		return v
	}, []);
}
function getUidsJson(ks) {
	return ks.reduce(function(v, k) {
		if (typeof k.author?.uid === "string") {
			v.push(k.author.uid);
		} else if (typeof k.uid === "string") {
			v.push(k.uid);
		}
		return v
	}, []);
}
function getUidsLiteral(value) {
	return value.split(/[\r\n]+/).map(line => line.replace(/#.*/, "").trim()).filter(line => line.length > 0);
}
function getIdsSync(value) {
	var i = 0, wr = /\s+/g, ur = /[\/\\]([1-9][0-9]{0,19})/, r = [];
	var m, t;
	while ((m = wr.exec(value)) !== null) {
		if (i < wr.lastIndex - m[0].length) {
			if (uidRegex.test(t = value.substring(i, wr.lastIndex - m[0].length))) {
				r.push(t);
			} else if ((m = t.match(ur)) !== null) {
				r.push(m[1]);
			}
		}
		i = wr.lastIndex;
	}
	if (i < value.length) {
		if (uidRegex.test(t = value.substring(i))) {
			r.push(t);
		} else if ((m = t.match(ur)) !== null) {
			r.push(m[1]);
		}
	}
	return r;
}
function getUidsSync(value) {
	var i = 0, wr = /\s+/g, ur = /[\/\\]@?([1-9][0-9]{0,19})/, r = [];
	var m, t;
	while ((m = wr.exec(value)) !== null) {
		if (i < wr.lastIndex - m[0].length) {
			if (uidRegex.test(t = value.substring(i, wr.lastIndex - m[0].length))) {
				r.push(t.replace(/^@/, ""));
			} else if ((m = t.match(ur)) !== null) {
				r.push(m[1]);
			}
		}
		i = wr.lastIndex;
	}
	if (i < value.length) {
		if (uidRegex.test(t = value.substring(i))) {
			r.push(t.replace(/^@/, ""));
		} else if ((m = t.match(ur)) !== null) {
			r.push(m[1]);
		}
	}
	return r;
}
function splitCmd(value, args) {
	args = args || [];
	for (var i = 0, e, c, t, sr = /[^ ]/g; i < value.length;) {
		switch (value.charCodeAt(i)) {
			case 32:
				sr.lastIndex = i + 1;
				i = (t = sr.exec(value)) !== null ? t.index : value.length;
				break;
			case 34:
				for (i++, e = 0, t = []; i < value.length; i++) {
					if ((c = value.charCodeAt(i)) === 34 && (++i === value.length || value.charCodeAt(i) !== 34)) {
						e = 1;
						break;
					}
					t.push(c);
				}
				if (e === 0) {
					throw new Error("Unescaped quotation mark detected.");
				}
				args.push(String.fromCharCode.apply(null, t));
				break;
			case 39:
				for (i++, e = 0, t = []; i < value.length; i++) {
					if ((c = value.charCodeAt(i)) === 39 && (++i === value.length || value.charCodeAt(i) !== 39)) {
						e = 1;
						break;
					}
					t.push(c);
				}
				if (e === 0) {
					throw new Error("Unescaped apostrophe detected.");
				}
				args.push(String.fromCharCode.apply(null, t));
				break;
			default:
				args.push(value.substring(i, (i = value.indexOf(" ", i + 1)) > -1 ? i : i = value.length));
				break;
		}
	}
	return args;
}
function random_user_agent() {
//	return user_agents.random().toString();
//	return (new user_agents).toString();
//	return user_agents[Math.floor(Math.random()*user_agents.length)];
	return "com.zhiliaoapp.musically/" + Math.random().toString().substr(2,10) + " (Linux; U; Android 7.1.2; en_US; SM-G973N; Build/PPR1.190810.011;tt-ok/3.10.0.2)";
}
function requestGet(uri, headers, callback) {
	try {
		var u = typeof uri === "string" ? new url.URL(uri) : uri, h, r,
		uri_block = u.host === "www.tikwm.com" || u.host === "tikwm.com" || uri_blocks.has(String(uri)),
		uri_headers = uri_block ? headers : {
			...headers,
			"Cookie": cookie + (cookie.length > 0 ? "; " : "") + (u.host === "www.tiktok.com" && cookie_web_api.length > 0 ? cookie_web_api : generateCookie()),
			"User-Agent": user_agents_random ? random_user_agent() : user_agent,
			...(u.host === "www.tiktok.com" ? {"Referer": "https://www.tiktok.com/"} : {})
		};
		if (u.protocol === "http:") {
			h = http;
		} else if (u.protocol === "https:") {
			h = https;
		} else {
			process.nextTick(callback, new Error("Only http and https is supported."));
			return;
		}
		if (debug) console_log("http/get: " + String(uri));
		if (debug) console_log(uri_headers);
		r = h.request({
//			ciphers: ciphers_shuffled,
			method: "GET",
			host: u.host,
			port: +u.port || 0,
			path: u.pathname + u.search,//+ u.hash
			timeout: 10000,
//			agent: new h.Agent({
//				keepAlive: true,
//				keepAliveMsecs: 5000
//			}),
			headers: uri_headers
		}, function(response) {
			if (callback === null) {
				return;
			}
			if (debug) console_log("http/" + response.statusCode + (+response.headers["content-length"] ? " (" + response.headers["content-length"] + " bytes)" : "") + ": " + String(uri) + (response.headers["location"] !== undefined ? " => " + response.headers["location"] : ""));
			if (response.statusCode >= 300 && response.statusCode < 400) {
				var l = response.headers["location"];
				if (l === undefined) {
					callback(new Error("Redirection without location."));
					callback = null;
//				} else if (l.indexOf("-dynamic-") !== -1) {
//					if (debug) console_log("ignoring redirect: " + l);
//					requestGet(uri, headers, callback);
				} else if (reject_sd && String(uri).includes("tikwm.com/video/media/hdplay/") && l.includes("&btag=80000")) {
					callback(reject_sd_error);
					callback = null;
				} else {
					try {
						var new_uri = new URL(l, String(uri));
						if (uri_block) uri_blocks.add(String(new_uri));
						requestGet(new_uri, headers, callback);
					} catch (e) {
						callback(e);
						callback = null;
					}
				}
				return;
			}
			if (response.statusCode !== 200 && response.statusCode !== 206) {
				callback(new Error(response.statusMessage));
				callback = null;
				return;
			}
			callback(null, response, r, String(uri));
			callback = null;
		}).on("error", function(error) {
			if (callback === null) {
				return;
			}
			callback(error);
			callback = null;
		}).on("timeout", function() {
			r.destroy();
			if (debug) console_error("timed_out: " + String(uri));
		}).end();
	} catch (e) {
		process.nextTick(callback, e);
		return;
	}
}
function requestGetJson(uri, headers, callback) {
	requestGet(uri, {
		...headers,
		"Accept-Encoding": "gzip, deflate, *"
	}, function(error, response, request) {
		if (error !== null) {
			callback(error);
			return;
		}
		var data_stream, d = "";
		switch(response.headers["content-encoding"]) {
			case "gzip":
				data_stream = zlib.createGunzip();
				response.pipe(data_stream);
				break;
			case "deflate":
				data_stream = zlib.createInflate();
				response.pipe(data_stream);
				break;
			default:
				data_stream = response;
				data_stream.setEncoding("utf8");
				break;
		}
		data_stream.on("data", function(data) {
			d += data;
		}).on("error", function(error) {
			if (callback === null) {
				return;
			}
			callback(error);
			callback = null;
		}).on("end", function() {
			if (callback === null) {
				return;
			}
			if (d.length === 0) {
				callback(new Error("Empty response."));
				return;
			}
//			print_log(d.includes("user_story") + " | " + uri);
			if (debug_data) console_log(d);
			try {
				var d2 = d.replace(/([\[:])?(?<![\d.]|\\".)(\d{15,})([,\}\]])/g, "$1\"$2\"$3"); // rancid
				callback(null, JSON.parse(d2));
			} catch (e) {
				try {
					callback(null, JSON.parse(d));
//					console_error(new Error("RegEx for preserving big numbers failed"));
//					fs.writeFileSync("/" + path.basename(__filename) + "_" + output_time + "_pass.json", d);
//					fs.writeFileSync("/" + path.basename(__filename) + "_" + output_time + "_fail.json", d2);
				} catch (e) {
					callback(e);
				}
			}
			callback = null;
		});
	});
}
function getUser(id, callback) {
	requestGet("https://www.tiktok.com/share/user/" + id, {}, function(error, response, request) {
		if (error !== null) {
			callback(error);
			return;
		}
		var m = request.path.match(/@([^?/]+)/);
		if (m === null) {
			callback(new Error("Could not find the user."));
		} else {
			callback(null, decodeURI(m[1]));
		}
	});
}
function getHashtag(id, callback) {
	requestGet("https://www.tiktok.com/share/challenge/" + id, {}, function(error, response, request) {
		if (error !== null) {
			callback(error);
			return;
		}
		var m = request.path.match(/tag\/([^?/]+)/);
		if (m === null) {
			callback(new Error("Could not find the hashtag."));
		} else {
			callback(null, decodeURI(m[1]));
		}
	});
}
function getIds(value, callback) {
	var i = 0, wr = /\s+/g, ur = /[\/\\]([1-9][0-9]{0,19})(?:;(v\w{31}\b))?/, r = [], f0 = function() {
		var m, t;
		while ((m = wr.exec(value)) !== null) {
			if (i < wr.lastIndex - m[0].length) {
				if (idRegex.test(t = value.substring(i, wr.lastIndex - m[0].length))) {
					r.push(t);
				} else if ((m = t.match(ur)) !== null) {
					r.push(m[1]);
				} else {
					i = wr.lastIndex;
					f1(t, false);
					return;
				}
			}
			i = wr.lastIndex;
		}
		if (i < value.length) {
			if (idRegex.test(t = value.substring(i))) {
				r.push(t);
			} else if ((m = t.match(ur)) !== null) {
				r.push(m[1]);
			} else {
				f1(t, true);
				return;
			}
		}
		callback(null, r);
	}, f1 = function(uri, last) {
		requestGet(uri, {}, function(error, response, request) {
			var m;
			if (error === null && (m = request.path.match(ur)) !== null) {
				r.push(m[1]);
			}
			if (last) {
				callback(null, r);
			} else {
				f0();
			}
		});
	};
	process.nextTick(f0);
}
function getVideoUrlList(value) {
	if (hevc) {
		if (typeof value.play_addr_265 === "object" && value.play_addr_265 !== null
		&& typeof value.play_addr_265.url_list === "object" && value.play_addr_265.url_list !== null && value.play_addr_265.url_list.length > 0) {
			return value.play_addr_265.url_list;
		} else if (typeof value.play_addr_bytevc1 === "object" && value.play_addr_bytevc1 !== null
		&& typeof value.play_addr_bytevc1.url_list === "object" && value.play_addr_bytevc1.url_list !== null && value.play_addr_bytevc1.url_list.length > 0) {
			return value.play_addr_bytevc1.url_list;
		}
	}
	if (typeof value.play_addr === "object" && value.play_addr !== null
	&& typeof value.play_addr.url_list === "object" && value.play_addr.url_list !== null && value.play_addr.url_list.length > 0) {
		return value.play_addr.url_list;
	}
	return null;
}
function getVideoWatermarked(value) {
	if (typeof value.download_addr === "object" && value.download_addr !== null
	&& typeof value.download_addr.url_list === "object" && value.download_addr.url_list !== null && value.download_addr.url_list.length > 0) {
		return value.download_addr.url_list[value.download_addr.url_list.length - 1];
	}
	return null;
}
function getVideoThumbnail(value) {
	if (typeof value.cover === "object" && value.cover !== null
	&& typeof value.cover.url_list === "object" && value.cover.url_list !== null && value.cover.url_list.length > 0) {
		return value.cover.url_list[value.cover.url_list.length - 1];
	}
	return null;
}
function getVideoThumbnailAnimated(value) {
	if (typeof value.dynamic_cover === "object" && value.dynamic_cover !== null
	&& typeof value.dynamic_cover.url_list === "object" && value.dynamic_cover.url_list !== null && value.dynamic_cover.url_list.length > 0) {
		return value.dynamic_cover.url_list[value.dynamic_cover.url_list.length - 1];
	}
	return null;
}
function getVideoImages(value) {
	if (typeof value.image_post_info === "object" && value.image_post_info !== null
	&& typeof value.image_post_info.images === "object" && value.image_post_info.images !== null && value.image_post_info.images.length > 0) {
		var r = [];
		for (var j = 0; j < value.image_post_info.images.length; j++) {
			if (typeof value.image_post_info.images[j] === "object" && value.image_post_info.images[j] !== null
			&& typeof value.image_post_info.images[j].display_image === "object" && value.image_post_info.images[j].display_image !== null
			&& typeof value.image_post_info.images[j].display_image.url_list === "object" && value.image_post_info.images[j].display_image !== null && value.image_post_info.images[j].display_image.url_list.length > 0) {
				r.push(value.image_post_info.images[j].display_image.url_list[value.image_post_info.images[j].display_image.url_list.length - 1]);
			}
		}
		return r;
	}
	return null;
}
function downloadAweme(filename, uri, test_file_bytes, callback) {
	var f = function(error, repeat) {
		if (error === reject_sd_error || repeat > 3 || !running) {
			callback(error !== null ? error : new Error("Exited unexpectedly."));
		} else {
			requestGet(uri, test_file_bytes ? {
				"Range": "bytes=" + magic_bytes_start + "-" + magic_bytes_end
			} : {}, function(error, response, request) {
				if (error === null) uri = request.protocol + "//" + request.host + request.path;
				if (error !== null) {
					f(error, repeat + 1);
				} else if (!+response.headers["content-length"]) {
					f(new Error("Response body is empty."), repeat + 1);
				} else if (test_file_bytes) {
					var file_bytes = "";
					response.on("data", function(data) {
						file_bytes += data.toString("hex");
					}).on("end", function() {
						if (debug) console_log("test_file_bytes: " + file_bytes + " | " + (file_bytes === magic_bytes) + " | " + filename);
						if (file_bytes === magic_bytes) {
							test_file_bytes = false;
							f(null, 0);
						} else {
							callback(new Error("Mundane bytes."));
						}
					}).on("error", function(error) {
						callback(error);
					});
				} else if (cached_content_lengths.hasOwnProperty(filename) && cached_content_lengths[filename] === +response.headers["content-length"]) {
					request.destroy();
					callback(new Error("Broken file."));
				} else {
					var
					progress = 0,
					progress_max,
					progress_idx,
					progress_bar,
					file_size = +response.headers["content-length"],
					file_size_pretty = printSize(file_size);
					print_log("downloading: " + filename);
					if (progress_bars) {
						progress_max = multi_progress.stream.rows,
						progress_idx = multi_progress.cursor,
						progress_bar = multi_progress.newBar(output_line("[:bar] :percent | :progress_pretty / :file_size_pretty", progress_idx+1, "~"), {
							renderThrottle: 60,
							width: 10,
							total: file_size
						});
					}
					var attempt_download = function(response, request, attempts=0) {
						var
						stream = fs.createWriteStream(filename, {flags: "a", start: progress}),
						stream_running = true,
						stream_finish = function() {
							if (stream_running === true) {
								stream_running = false;
								request.destroy();
								stream.close();
								if (progress_bars && (multi_progress.bars.length-progress_idx) < progress_max) {
									multi_progress.move(progress_idx);
									progress_bar.render(undefined, true);
//									progress_bar.terminate();
								}
							}
						};
						if (attempts !== 0) {
							console_log("resuming: " + filename + " | " + attempts + " / " + max_resumes);
						}
						stream.on("close", function() {
							stream_running = false;
//							console_log("check_size: " + filename);
							fs.stat(filename, function(error, stats) {
								if (error !== null) {
									f(error, repeat + 1);
								} else if (stats.size !== file_size) {
									progress = stats.size;
									console_log("mismatch: " + filename + " | " + printSize(progress) + " / " + file_size_pretty);
									re_attempt_download(new Error("File size doesn't match content-length."), attempts + 1);
								} else if (callback !== null) {
									cached_content_lengths[filename] = file_size;
									callback(null);
								}
							});
						}).on("error", stream_finish);
						response.on("data", function(data) {
							if (progress_bars && (multi_progress.bars.length-progress_idx) < progress_max) {
								progress += Buffer.byteLength(data);
								progress_bar.update(progress/file_size, {
									progress_pretty: printSize(progress),
									file_size_pretty: file_size_pretty
								});
							}
							stream.write(data);
							if (!running) {
//								running = true;
								stream_finish();
							}
						}).on("end", stream_finish).on("error", stream_finish);
						request.removeAllListeners("timeout").on("timeout", function() {
							stream_finish();
							if (debug) console_error("timed_out: " + filename);
						});
//						request.setTimeout(1000);
					},
					re_attempt_download = function(error, attempts) {
						if (attempts > max_resumes || progress > file_size || !running) {
							fs.unlink(filename, function() {
								f(error, repeat + 1);
							});
						} else {
//							setTimeout(function() {
								requestGet(uri, {
									"Range": "bytes=" + progress + "-"
								}, function(error, response, request) {
									if (error !== null) {
										re_attempt_download(error, repeat + 1);
									} else {
										attempt_download(response, request, attempts);
									}
								});
//							}, 1000);
						}
					};
					attempt_download(response, request);
				}
			});
		}
	};
	f(null, 0);
}
function file_name(video, file_ext, file_tag, add_description=false, windows_long_path=false, metadata_dir=false) {
	var file_sub_dirs = [];
	if (metadata_dir && metadata_dirs && path.isAbsolute(metadata_dir_name)) file_sub_dirs.push(metadata_dir_name);
	else if (video.is_story && story_dirs && path.isAbsolute(story_dir_name)) file_sub_dirs.push(story_dir_name);
	if (uid_directories) file_sub_dirs.push("@" + video.uid);
	if (metadata_dir && metadata_dirs) {
		if (!path.isAbsolute(metadata_dir_name)) file_sub_dirs.push(metadata_dir_name);
	}
	else if (video.is_story && story_dirs && !path.isAbsolute(story_dir_name)) file_sub_dirs.push(story_dir_name);
	var file_dir = file_sub_dirs.join(path.sep);
	if (file_ext === "mp4_wm") {
		file_ext = "mp4",
		file_tag = tag_wm ? file_tag_wm : file_tag;
	} else if (file_ext === "mp4_sd") {
		file_ext = "mp4",
		file_tag = tag_sd ? file_tag_sd : file_tag;
	}
	var base_file_name = function(v) {
		return video.id
		+ (tag_uid && video.uid.length > 0 ? "@" + video.uid : "")
		+ (tag_unique_id && video.unique_id.length > 0 ? "@" + video.unique_id : "")
		+ (d.length > 0 ? " " + d : "")
		+ (v.length > 0 ? " " + v : "")
		+ (tag_mutual && video.is_mutual ? file_tag_delimiter + file_tag_mutual : "")
		+ (tag_stories && video.is_story ? file_tag_delimiter + file_tag_stories : "")
		+ (file_tag.length > 0 ? file_tag_delimiter + file_tag : "")
//		+ "." + file_ext;
		+ (file_ext.charAt(0) !== path.sep ? "." : "") + file_ext;
	},
	d = date && video.time > 0 ? createTimeToFilename(video.time) : "",
	t = add_description && video.desc.length > 0 ? sanitizeFilename(video.desc) : "",
	n = base_file_name(t);
	if (description_limit > 0 && t.length > 0 && Buffer.byteLength(n) > description_limit) {
		t = t.substring(0, description_limit - base_file_name("").length).trim(),
		n = base_file_name(t);
		while (t.length > 0 && Buffer.byteLength(n) > description_limit) {
			t = t.slice(0, -1).trim(),
			n = base_file_name(t);
		}
	}
	if (file_dir.length > 0) {
		n = file_dir + path.sep + n;
	}
	if (windows_long_path !== false) {
//		https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
		n = "\\\\\?\\" + file_path_absolute(n);
	}
	return n;
}
function ensureAweme(video, file_ext, callback) {
	var add_description = description && file_ext.charAt(0) !== path.sep,
	verify = require_ffprobe && file_ext === "mp4",
	file_tag = verify ? file_tag_verify : file_tag_default,
	file = file_name(video, file_ext, file_tag, add_description),
	file_whole = file,
	file_trimmed = false;
	if (add_description && video.desc.length > 0) {
		if (Buffer.byteLength(path.basename(file)) > 255) {
			file = file_name(video, file_ext, file_tag, add_description = false);
		} else if (windows_paths && file_path_absolute(file).length > windows_max_path) {
			file = file_name(video, file_ext, file_tag, add_description = false),
			file_trimmed = true;
		}
	}
	if (ensureFileDirectorySync(file, callback) !== null) return;
	var uris = video[file_ext],
	download_attempt = 1,
	download_attempts = file_ext === "mp4" ? (test_magic_bytes ? uris.length*2 : uris.length)-1 : max_retries,
	test_file_exists = function(test_file, callback_) {
		fs.access(test_file, function(error) {
			if (debug) console_log("test_file_exists: " + test_file);
			if (error === null) {
				if (debug) console_log("file_exists: " + test_file);
				callback_(null, test_file, true);
			} else if (error.code === "ENOENT" || error.code === "ENAMETOOLONG") {
				callback_(error, test_file);
			} else {
				callback(error, test_file);
			}
		});
	}, test_file_default = function() {
		test_file_exists(file_name(video, file_ext, file_tag_default, add_description), function(error, test_file, exists) {
			if (exists) {
				callback(null, test_file, true);
			} else if (add_description && video.desc.length > 0) {
				test_file_exists(file_name(video, file_ext, file_tag_default, false), function(error, test_file, exists) {
					if (exists) {
						callback(null, test_file, true);
					} else if (verify && rename_hevc) {
						test_file_hevc();
					} else if (verify) {
						test_file_verify();
					} else {
						test_file_create();
					}
				});
			} else if (verify && rename_hevc) {
				test_file_hevc();
			} else if (verify) {
				test_file_verify();
			} else {
				download_file();
			}
		})
	}, test_file_hevc = function() {
		test_file_exists(file_name(video, file_ext, file_tag_hevc, add_description), function(error, test_file, exists) {
			if (exists) {
				callback(null, test_file, true);
			} else if (add_description && video.desc.length > 0) {
				test_file_exists(file_name(video, file_ext, file_tag_hevc, false), function(error, test_file, exists) {
					if (exists) {
						callback(null, test_file, true);
					} else if (verify) {
						test_file_verify();
					} else {
						test_file_create();
					}
				});
			} else if (verify) {
				test_file_verify();
			} else {
				download_file();
			}
		})
	}, test_file_verify = function() {
		test_file_exists(file_name(video, file_ext, file_tag, add_description), function(error, test_file, exists) {
			if (exists) {
				download_attempt = 0;
				verify_file();
			} else if (add_description && video.desc.length > 0) {
				test_file_exists(file_name(video, file_ext, file_tag, false), function(error, test_file, exists) {
					if (exists) {
						add_description = false;
						download_attempt = 0;
						verify_file();
					} else {
						test_file_create();
					}
				});
			} else {
				download_file();
			}
		})
	}, test_file_create = function() {
		fs.open(file, "w", function(error, fd) {
			if (debug) console_log("test_file_create: " + file);
			if (error !== null) {
				if (debug) console_log("fail_file_create: " + file);
				file = file_name(video, file_ext, file_tag, add_description = false);
				download_file();
			} else {
				fs.close(fd, function(error) {
					fs.unlink(file, download_file);
				});
			}
		});
	}, download_file = function() {
		if (file_ext === "mp4" && (video.is_story || video.is_mutual || video.is_private)) {
			callback(reject_sd_error, file);
			return;
		}
		var uri = file_ext === "mp4" ? uris[(download_attempt-1) % uris.length] : uris,
		test_file_bytes = file_ext === "mp4" && test_magic_bytes;
		if (test_file_bytes && !(test_file_bytes = uris.length > download_attempt-1)) {
			console_log("retrying (" + (download_attempt-1) + "/" + download_attempts + "): " + file);
		}
		if (download_attempt === 1 && file !== file_whole) {
			if (file_trimmed) {
				print_log("path too long: " + file_whole);
			} else {
				print_log("name too long: " + file_whole);
			}
		}
		downloadAweme(file, uri, test_file_bytes, function(error) {
			if (error !== null) {
				re_download_file(error, file);
			} else if (verify) {
				verify_file();
			} else {
				callback(null, file);
			}
		});
	}, re_download_file = function(error, file) {
		if (error === null) {
			callback(null, file);
		} else if (error === reject_sd_error || download_attempt++ > download_attempts || !running) {
//			print_log("abandoning: " + file);
			callback(error, file);
		} else {
			download_file();
		}
	}, verify_file = function(windows_long_path=false) {
		var probe_file = file_name(video, file_ext, file_tag, add_description, windows_long_path);
		ffprobe(probe_file, {path: ffprobe_path}, function(error, response) {
			if (debug) console_log("verify_file: " + probe_file);
//			error = new Error("fail_verify: " + probe_file);
			if (error !== null) {
				if (windows_long_path === false && process.platform === "win32") {
					verify_file(true);
				} else {
					fail_file(error);
				}
			} else if (typeof response !== "object" || response === null
			|| typeof response.streams !== "object" || response.streams === null
			|| response.streams.length === 0) {
				fail_file(new Error(probe_file));
			} else {
				for (var is_hevc = false, j = 0; j < response.streams.length; j++) {
					if (is_hevc = response.streams[j].codec_type === "video" && response.streams[j].codec_name === "hevc") {
						break;
					}
				}
				var new_file_tag = is_hevc && rename_hevc ? file_tag_hevc : file_tag_default;
				if (file_tag !== new_file_tag) {
//					if (windows_long_path !== false) { // was just a warning, but led to too much confusion
//						print_log("path too long: " + probe_file);
//					}
					var new_file = file_name(video, file_ext, new_file_tag, add_description, windows_long_path);
					fs.rename(file, new_file, function(rename_error) {
						re_download_file(rename_error, new_file);
						if (rename_error === null && is_hevc && convert_hevc) {
							convert_file(new_file, file_name(video, file_ext, file_tag_h264, add_description, windows_long_path));
						}
					});
				} else {
					re_download_file(null, probe_file);
					if (is_hevc && convert_hevc) {
						convert_file(probe_file, file_name(video, file_ext, file_tag_h264, add_description, windows_long_path));
					}
				}
			}
		});
	}, fail_file = function(error) {
		var error_file = file_name(video, file_ext, file_tag_error, add_description);
		fs.rename(file, error_file, function(rename_error) {
			if (rename_error !== null) {
				console_error(rename_error);
			} else {
				print_log("renamed: " + error_file);
			}
			re_download_file(error, file);
		});
	}, convert_file = function(input_file, output_file) {
		if (active_converts >= max_converts) {
			queued_converts.push(arguments);
		} else {
			active_converts += 1;
			var queue_next = function() {
				active_converts -= 1;
				if (queued_converts.length > 0) {
					convert_file(...queued_converts.shift());
				}
			};
			fs.access(output_file, function(error) {
				if (debug) console_log("test_file_exists: " + output_file);
				if (error !== null) {
					if (error.code !== "ENOENT") {
						console_error(error);
						queue_next();
					} else {
						if (debug) console_log("file_exists: " + output_file);
						print_log("converting: " + input_file);
						var proc = child_process.spawn(ffmpeg_path, [
							"-i", input_file,
							"-map", "0",

							"-c:v", "libx264",
//							"-crf", "18",
							"-crf", "23",
//							"-preset", "veryslow",
							"-preset", "veryfast",

//							"-c:v", "h264_nvenc",
//							"-cq", "21",

							"-pix_fmt", "yuv420p",
							"-b:v", "0",
							"-c:a", "copy",
							"-movflags", "+faststart+use_metadata_tags",
							output_file
						], {stdio: "ignore", detached: true}); // unlink it from the node process, in case that's closed for some reason while ffmpeg is still converting
						proc.on("close", function() {
							print_log("converted: " + output_file);
							queue_next();
						});
					}
				} else {
					queue_next();
				}
			});
		}
	};
	test_file_default();
}
function ensureFile(video, file_name_generator, callback) {
	var add_description = description,
	file = file_name_generator(add_description),
	file_whole = file,
	file_trimmed = false;
	if (add_description && video.desc.length > 0) {
		if (Buffer.byteLength(path.basename(file)) > 255) {
			file = file_name_generator(add_description = false);
		} else if (windows_paths && file_path_absolute(file).length > windows_max_path) {
			file = file_name_generator(add_description = false),
			file_trimmed = true;
		}
	}
	if (ensureFileDirectorySync(file, callback) !== null) return;
	var test_file_exists = function(test_file, callback_) {
		fs.access(test_file, function(error) {
			if (debug) console_log("test_file_exists: " + test_file);
			if (error === null) {
				if (debug) console_log("file_exists: " + test_file);
				callback_(null, test_file, true);
			} else if (error.code === "ENOENT" || error.code === "ENAMETOOLONG") {
				callback_(error, test_file);
			} else {
				callback(error, test_file);
			}
		});
	}, test_file_default = function() {
		test_file_exists(file_name_generator(add_description), function(error, test_file, exists) {
			if (exists) {
				callback(null, test_file, true);
			} else if (add_description && video.desc.length > 0) {
				test_file_exists(file_name_generator(false), function(error, test_file, exists) {
					if (exists) {
						callback(null, test_file, true);
					} else {
						test_file_create();
					}
				});
			} else {
				download_file();
			}
		})
	}, test_file_create = function() {
		fs.open(file, "w", function(error, fd) {
			if (debug) console_log("test_file_create: " + file);
			if (error !== null) {
				if (debug) console_log("fail_file_create: " + file);
				file = file_name_generator(add_description = false);
				download_file();
			} else {
				fs.close(fd, function(error) {
					fs.unlink(file, download_file);
				});
			}
		});
	}, download_file = function() {
		if (file !== file_whole) {
			if (file_trimmed) {
				print_log("path too long: " + file_whole);
			} else {
				print_log("name too long: " + file_whole);
			}
		}
		callback(null, file);
	};
	test_file_default();
}
function encryptParams(v) {
	var cipher = crypto.createCipheriv("aes-128-cbc", "webapp1.0+202106", "webapp1.0+202106");
	var encrypted = cipher.update(v, "utf8", "base64");
	return encrypted + cipher.final("base64");
}
function decryptParams(v) {
	var decipher = crypto.createDecipheriv("aes-128-cbc", "webapp1.0+202106", "webapp1.0+202106");
	var decrypted = decipher.update(v, "base64", "utf8");
	return decrypted + decipher.final("utf8");
}
function requestTiktok(uri, callback) {
	var f = function(error, repeat) {
		if (repeat > 9 || (ignore_errors && repeat > 0) || !running) {
			callback(error !== null ? error : new Error("Exited unexpectedly."));
			return;
		}
		requestGetJson("https://www.tiktok.com" + uri, {
			"x-tt-params": encryptParams(path.basename(uri).substring(1))
		}, function(error, data) {
			if (error !== null) {
				f(error, repeat + 1);
			} else if (typeof data !== "object" || data === null || typeof data.statusCode !== "number" || data.statusCode !== 0) {
				f(new Error("Unexpected response."), repeat + 1);
			} else {
				if (debug_data) console_log(data);
				callback(null, data);
			}
		});
	};
	f(null, 0);
}
function signAwemeCachedFile(uri) {
	var uri_hash = crypto.createHash("md5").update(uri).digest("hex");
	var request_aweme_cache_file = request_aweme_cache_dir + path.sep + uri_hash + ".json";
	return file_path_absolute(request_aweme_cache_file, root_directory);
}
function signAwemeCached(uri) {
	try {
		return JSON.parse(fs.readFileSync(signAwemeCachedFile(uri), {flag: "r", encoding: "utf8"}));
	} catch (e) {}
}
function signAwemeCache(uri, uri_full, f0, callback) {
	var request_aweme_cache_file;
	signAweme(path.basename(uri_full).substring(1), function(error, headers) {
		if (error !== null || ensureFileDirectorySync(request_aweme_cache_file = signAwemeCachedFile(uri)) !== null) {
			callback(error !== null ? error : request_aweme_cache_file, undefined, uri);
		} else {
			fs.writeFile(request_aweme_cache_file, JSON.stringify({
				"uri": uri_full,
				"x-argus": headers["X-Argus"],
				"x-ladon": headers["X-Ladon"]
			}), function(error) {
				if (error !== null) {
					console_error(error);
				} else if (debug) {
					console_log("sign_aweme_save: " + uri + " => " + request_aweme_cache_file);
				}
				f0(uri_full, headers);
			});
		}
	});
}
function signAweme(params, callback) {
	setTimeout(requestGetJson, 1000, "https://www.tikwm.com/api/service/sign?params=" + encodeURIComponent(params), {}, function(error, data) {
		if (error === null) {
			if (typeof data !== "object" || data === null || typeof data.code !== "number") {
				error = new Error("Unexpected response.");
			} else if (data.code !== 0) { // success
				error = new Error(data.code + (typeof data.msg === "string" && data.msg.length > 0 ? " (" + data.msg + ")" : ""));
			}
		}
		callback(error, data?.data);
	});
}
function requestAweme(host, uri, callback) {
	var f = function(error, repeat) {
		if (repeat > (cycle_hosts ? aweme_hosts.length*3 : 9) || !running) {
			callback(error !== null ? error : new Error("Exited unexpectedly."), undefined, uri);
			return;
		}
		if (cycle_hosts && repeat > 0) host = aweme_hosts[(repeat-1) % aweme_hosts.length];
		var u, f0 = function(uri_full, headers) {
			requestGetJson("https://" + host + uri_full, headers, function(error, data) {
				if (error === null) {
					if (typeof data !== "object" || data === null || typeof data.status_code !== "number") {
						error = new Error("Unexpected response.");
					} else if (![0,5,8,2053].includes(data.status_code)) { // success / invalid parameters / login expired / video has been removed
						error = awemeError(data);
					}
				}
				if (error !== null) {
					deviceId = generateDeviceId();
//					iId = generateDeviceId();
					deviceVersion = generateBase62(8);
					f(error, repeat + 1);
				} else {
					if (debug_data) console_log(data);
					callback(null, data, uri);
				}
			});
		};
		if ((u = request_aweme_static)
		|| (request_aweme_cache && (u = signAwemeCached(uri)))) {
			if (debug) console_log("sign_aweme_load: " + u.uri);
			f0(u.uri, {
				"X-Argus": u["x-argus"],
				"X-Ladon": u["x-ladon"]
			});
		} else {
			var d = new Date(), t = d.getTime() - d.getTimezoneOffset() * 60000, uri_full = uri
			+ "&address_book_access=1"
			+ "&region=US"
			+ "&ts=" + Math.floor(t / 1000)
			+ "&timezone_name=Etc%2FGMT"
			+ "&device_type=Pixel%20" + deviceVersion
//			+ "&iid=" + iId
			+ "&locale=en"
			+ "&app_type=normal"
			+ "&resolution=1080*1920"
			+ "&aid=" + aid
			+ "&app_name=musical_ly"
			+ "&_rticket=" + t
			+ "&device_platform=android"
			+ "&dpi=441"
			+ "&cpu_support64=false"
			+ "&sys_region=US"
			+ "&timezone_offset=0"
			+ "&device_id=" + deviceId
			+ "&pass-route=1"
			+ "&device_brand=google"
			+ "&os_version=10"
			+ "&op_region=US"
			+ "&app_language=en"
			+ "&pass-region=1"
			+ "&language=en"
			+ "&channel=googleplay"
			+ "&manifest_version_code=240903"
			+ "&update_version_code=240903"
			+ "&version_code=240903"
			+ "&ab_version=24.9.3"
			+ "&build_number=24.9.3"
			+ "&version_name=24.9.3";
			if (request_aweme_cache) {
				if (debug) console_log("sign_aweme_queue: " + uri);
				signAwemeCache(uri, uri_full, f0, callback);
			} else {
				f0(uri_full, {});
			}
		}
	};
	f(null, 0);
}
function awemeError(data) {
	return new Error(data.status_code + (typeof data.status_msg === "string" && data.status_msg.length > 0 ? " (" + data.status_msg + ")" : ""));
}
function infoUser(idOrName, callback) {
	var f = function(error, data) {
		if (error !== null) {
			console_error(error);
		} else if (typeof data !== "object" || data === null
		|| typeof data.userInfo !== "object" || data.userInfo === null
		|| typeof data.userInfo.user !== "object" || typeof data.userInfo.user === null
		|| typeof data.userInfo.stats !== "object" || typeof data.userInfo.stats === null) {
			console_error(new Error("Unexpected response."));
		} else {
			console_log("id: " + data.userInfo.user.id);
			console_log("unique id: " + data.userInfo.user.uniqueId);
			console_log("secure id: " + data.userInfo.user.secUid);
			console_log("nickname: " + data.userInfo.user.nickname);
			if (typeof data.userInfo.user.createTime === "number" && data.userInfo.user.createTime !== 0) {
				console_log("created: " + createTimeToString(data.userInfo.user.createTime));
			}
			console_log("verified: " + data.userInfo.user.verified);
			console_log("private: " + data.userInfo.user.secret);
			console_log("likes: " + data.userInfo.user.openFavorite);
			console_log("videos: " + data.userInfo.stats.videoCount);
			console_log("favorites: " + data.userInfo.stats.diggCount);
			console_log("favorited: " + data.userInfo.stats.heartCount);
			console_log("followings: " + data.userInfo.stats.followingCount);
			console_log("followers: " + data.userInfo.stats.followerCount);
			if (typeof data.userInfo.user.avatarLarger === "string" && data.userInfo.user.avatarLarger.length > 0) {
				console_log("avatar: " + data.userInfo.user.avatarLarger);
			}
			if (typeof data.userInfo.user.signature === "string" && data.userInfo.user.signature.length > 0) {
				console_log(data.userInfo.user.signature);
			}
			if (typeof data.userInfo.user.bioLink === "object" && data.userInfo.user.bioLink !== null
			&& typeof data.userInfo.user.bioLink.link === "string" && data.userInfo.user.bioLink.link.length > 0) {
				console_log(data.userInfo.user.bioLink.link);
			}
		}
		callback(null);
	};
	if (idOrName.charCodeAt(0) === 64) {
		requestTiktok("/node/share/user/@" + encodeURI(idOrName.substring(1)) + "?user_agent=", f);
	} else {
		getUser(idOrName, function(error, name) {
			if (error !== null) {
				console_error(error);
				callback(null);
				return;
			}
			requestTiktok("/node/share/user/@" + encodeURI(name) + "?user_agent=", f);
		});
	}
}
function infoMusic(id, callback) {
	requestTiktok("/node/share/music/_-" + id + "?user_agent=", function(error, data) {
		if (error !== null) {
			console_error(error);
		} else if (typeof data !== "object" || data === null
		|| typeof data.musicInfo !== "object" || data.musicInfo === null
		|| typeof data.musicInfo.music !== "object" || typeof data.musicInfo.music === null
		|| typeof data.musicInfo.stats !== "object" || typeof data.musicInfo.stats === null) {
			console_error(new Error("Unexpected response."));
		} else {
			console_log("id: " + data.musicInfo.music.id);
			console_log("title: " + data.musicInfo.music.title);
			if (typeof data.musicInfo.music.authorName === "string" && data.musicInfo.music.authorName.length > 0) {
				console_log("author: " + data.musicInfo.music.authorName);
			}
			console_log("videos: " + data.musicInfo.stats.videoCount);
			if (typeof data.musicInfo.music.coverLarge === "string" && data.musicInfo.music.coverLarge.length > 0) {
				console_log("cover: " + data.musicInfo.music.coverLarge);
			}
			if (typeof data.musicInfo.music.playUrl === "string" && data.musicInfo.music.playUrl.length > 0) {
				console_log("play: " + data.musicInfo.music.playUrl);
			}
			if (typeof data.musicInfo.author === "object" && typeof data.musicInfo.author !== null) {
				console_log("user id: " + data.musicInfo.author.id);
				console_log("user unique id: " + data.musicInfo.author.uniqueId);
				console_log("user secure id: " + data.musicInfo.author.secUid);
				console_log("user nickname: " + data.musicInfo.author.nickname);
				console_log("user verified: " + data.musicInfo.author.verified);
				console_log("user private: " + data.musicInfo.author.secret);
				console_log("user likes: " + data.musicInfo.author.openFavorite);
				if (typeof data.musicInfo.author.avatarLarger === "string" && data.musicInfo.author.avatarLarger.length > 0) {
					console_log("user avatar: " + data.musicInfo.author.avatarLarger);
				}
				if (typeof data.musicInfo.author.signature === "string" && data.musicInfo.author.signature.length > 0) {
					console_log(data.musicInfo.author.signature);
				}
			}
		}
		callback(null);
	});
}
function infoHashtag(idOrTag, callback) {
	var f = function(error, data) {
		if (error !== null) {
			console_error(error);
		} else if (typeof data !== "object" || data === null
		|| typeof data.challengeInfo !== "object" || data.challengeInfo === null
		|| typeof data.challengeInfo.challenge !== "object" || typeof data.challengeInfo.challenge === null
		|| typeof data.challengeInfo.stats !== "object" || typeof data.challengeInfo.stats === null) {
			console_error(new Error("Unexpected response."));
		} else {
			console_log("id: " + data.challengeInfo.challenge.id);
			console_log("tag: " + data.challengeInfo.challenge.title);
			console_log("videos: " + data.challengeInfo.stats.videoCount);
			console_log("views: " + data.challengeInfo.stats.viewCount);
			if (typeof data.challengeInfo.challenge.profileLarger === "string" && data.challengeInfo.challenge.profileLarger.length > 0) {
				console_log("cover: " + data.challengeInfo.challenge.profileLarger);
			}
			if (typeof data.challengeInfo.challenge.desc === "string" && data.challengeInfo.challenge.desc.length > 0) {
				console_log(data.challengeInfo.challenge.desc);
			}
		}
		callback(null);
	};
	if (idOrTag.charCodeAt(0) === 35) {
		requestTiktok("/node/share/tag/" + encodeURI(idOrTag.substring(1)) + "?user_agent=", f);
	} else {
		getHashtag(idOrTag, function(error, name) {
			if (error !== null) {
				console_error(error);
				callback(null);
				return;
			}
			requestTiktok("/node/share/tag/" + encodeURI(name) + "?user_agent=", f);
		});
	}
}
function infoVideo(id, callback) {
	requestTiktok("/node/share/video/@_/" + id + "?user_agent=", function(error, data) {
		if (error !== null) {
			console_error(error);
			callback(null);
			return;
		} else if (typeof data !== "object" || data === null
		|| typeof data.itemInfo !== "object" || data.itemInfo === null
		|| typeof data.itemInfo.itemStruct !== "object" || typeof data.itemInfo.itemStruct === null
		|| typeof data.itemInfo.itemStruct.stats !== "object" || typeof data.itemInfo.itemStruct.stats === null
		|| typeof data.itemInfo.itemStruct.video !== "object" || typeof data.itemInfo.itemStruct.video === null) {
			console_error(new Error("Unexpected response."));
			callback(null);
			return;
		}
		console_log("id: " + data.itemInfo.itemStruct.id);
		console_log("created: " + createTimeToString(+data.itemInfo.itemStruct.createTime));
		console_log("views: " + data.itemInfo.itemStruct.stats.playCount);
		console_log("likes: " + data.itemInfo.itemStruct.stats.diggCount);
		console_log("comments: " + data.itemInfo.itemStruct.stats.commentCount);
		console_log("shares: " + data.itemInfo.itemStruct.stats.shareCount);
		if (typeof data.itemInfo.itemStruct.video.cover === "string" && data.itemInfo.itemStruct.video.cover.length > 0) {
			console_log("cover: " + data.itemInfo.itemStruct.video.cover);
		}
		if (typeof data.itemInfo.itemStruct.video.dynamicCover === "string" && data.itemInfo.itemStruct.video.dynamicCover.length > 0) {
			console_log("dynamic cover: " + data.itemInfo.itemStruct.video.dynamicCover);
		}
		requestAweme(aweme_host_details, "/aweme/v1/aweme/detail/?aweme_id=" + data.itemInfo.itemStruct.id, function(error, data_) {
			var u;
			if (error !== null) {
				//console_error(error);
				u = null;
			} else if (typeof data_ === "object" && data_ !== null
			&& typeof data_.aweme_detail === "object" && data_.aweme_detail !== null
			&& typeof data_.aweme_detail.video === "object" && data_.aweme_detail.video !== null) {
				u = getVideoUrlList(data_.aweme_detail.video);
			} else {
				u = null;
			}
			console_log("play: " + (u !== null ? quality ? getQualityUri_(u[u.length - 1]) : u[u.length - 1] : data.itemInfo.itemStruct.video.playAddr));
			if (typeof data.itemInfo.itemStruct.desc === "string" && data.itemInfo.itemStruct.desc.length > 0) {
				console_log(data.itemInfo.itemStruct.desc);
			}
			if (typeof data.itemInfo.itemStruct.music === "object" && data.itemInfo.itemStruct.music !== null) {
				console_log("music id: " + data.itemInfo.itemStruct.music.id);
				console_log("music title: " + data.itemInfo.itemStruct.music.title);
				if (typeof data.itemInfo.itemStruct.music.authorName === "string" && data.itemInfo.itemStruct.music.authorName.length > 0) {
					console_log("music author: " + data.itemInfo.itemStruct.music.authorName);
				}
				if (typeof data.itemInfo.itemStruct.music.coverLarge === "string" && data.itemInfo.itemStruct.music.coverLarge.length > 0) {
					console_log("music cover: " + data.itemInfo.itemStruct.music.coverLarge);
				}
				if (typeof data.itemInfo.itemStruct.music.playUrl === "string" && data.itemInfo.itemStruct.music.playUrl.length > 0) {
					console_log("music play: " + data.itemInfo.itemStruct.music.playUrl);
				}
			}
			if (typeof data.itemInfo.itemStruct.author === "object" && data.itemInfo.itemStruct.author !== null) {
				console_log("user id: " + data.itemInfo.itemStruct.author.id);
				console_log("user unique id: " + data.itemInfo.itemStruct.author.uniqueId);
				console_log("user secure id: " + data.itemInfo.itemStruct.author.secUid);
				console_log("user nickname: " + data.itemInfo.itemStruct.author.nickname);
				console_log("user verified: " + data.itemInfo.itemStruct.author.verified);
				console_log("user private: " + data.itemInfo.itemStruct.author.secret);
				console_log("user likes: " + data.itemInfo.itemStruct.author.openFavorite);
				if (typeof data.itemInfo.itemStruct.authorStats === "object" && data.itemInfo.itemStruct.authorStats !== null) {
					console_log("user videos: " + data.itemInfo.itemStruct.authorStats.videoCount);
					console_log("user favorites: " + data.itemInfo.itemStruct.authorStats.diggCount);
					console_log("user favorited: " + data.itemInfo.itemStruct.authorStats.heartCount);
					console_log("user followings: " + data.itemInfo.itemStruct.authorStats.followingCount);
					console_log("user followers: " + data.itemInfo.itemStruct.authorStats.followerCount);
				}
				if (typeof data.itemInfo.itemStruct.author.avatarLarger === "string" && data.itemInfo.itemStruct.author.avatarLarger.length > 0) {
					console_log("user avatar: " + data.itemInfo.itemStruct.author.avatarLarger);
				}
				if (typeof data.itemInfo.itemStruct.author.signature === "string" && data.itemInfo.itemStruct.author.signature.length > 0) {
					console_log(data.itemInfo.itemStruct.author.signature);
				}
			}
			callback(null);
		});
	});
}
function download_item(video, download) {
	if (metadata_save_each && video["metadata"] !== null) {
		metadata_save_each_to_file(video);
	}
	if (download_videos && download_wm && video["mp4_wm"] !== null) {
		download(video, "mp4_wm");
	}
	if (download_videos && download_sd && video["mp4_sd"] !== null) {
		download(video, "mp4_sd");
	}
	if (download_videos && download_hd && video["mp4"] !== null) {
		download(video, "mp4");
	}
	if (download_thumbs && video["jpg"] !== null) {
		download(video, "jpg");
	}
	if (download_thumbs_anim && video["webp"] !== null) {
		download(video, "webp");
	}
	if (download_videos && video["mp3"] !== null && video["images"] !== null) {
		if (zip_photos) {
			download_zip_photos(video);
		} else {
			download_photos(video, download);
		}
	}
}
function download_photos(video, download) {
	var pad_len = Math.max(2, (video.images.length+1).toString().length);
	var ext = path.sep + video.id + ".mp3";
	video[ext] = video["mp3"];
	download(video, ext);
	for (var j = 0; j < video.images.length; j++) {
		var ext = path.sep + video.id + file_tag_delimiter + (j+1).toString().padStart(pad_len, "0") + ".jpg";
		video[ext] = video.images[j];
		download(video, ext);
	}
}
function download_zip_photos(video) {
	if (active_zips >= max_zips) {
		queued_zips.push(arguments);
	} else {
		active_zips += 1;
		var queue_next = function(error) {
			if (error !== undefined && error !== null) {
				console_error(error);
			}
			active_zips -= 1;
			if (queued_zips.length > 0) {
				download_zip_photos(...queued_zips.shift());
			}
		};
		ensureFile(video, add_description => file_name(video, "zip", file_tag_default, add_description), function(error, file, exists) {
			if (error !== null) {
				console_error(error);
				queue_next();
			} else if (exists) {
				queue_next();
			} else {
				print_log("downloading: " + file);
				var items = new Set(),
				item_dirs = new Set(),
				item_count = 0;
				download_photos(video, function(video, file_ext) {
					item_count += 1;
					ensureAweme(video, file_ext, function(error, item, exists) {
						if (error !== null) {
							if (debug || error !== reject_sd_error) print_log("failed: " + item + " | " + error.message);
							queue_next();
						} else {
							items.add(item);
							item_dirs.add(path.dirname(item));
							if (item_count === items.size) {
								item_dirs.forEach(function(item_dir) {
									fs.readdirSync(item_dir).forEach(function(item_name) {
										var item = item_dir + path.sep + item_name;
										if (items.has(item)) {
											item_count -= 1;
										} else {
											item_count = -1;
											console_error("non_photo_file: " + item);
										}
									})
								});
								if (item_count === 0) {
									var output = fs.createWriteStream(file),
									archive = archiver("zip"),
									archive_finish = function() {
										print_log("downloaded: " + file);
										queue_next();
										items.forEach(function(item) {
											if (debug) console_log("file_delete: " + item);
											fs.unlink(item, function(error) {
												if (error !== null) {
													console_error(error);
												} else {
													if (debug) console_log("file_deleted: " + item);
													item_count += 1;
													if (item_count === items.size) {
														item_dirs.forEach(function(item_dir) {
															if (debug) console_log("dir_delete: " + item_dir);
															fs.rmdir(item_dir, function(error) {
																if (error !== null) {
																	console_error(error);
																} else {
																	if (debug) console_log("dir_deleted: " + item_dir);
																}
															});
														});
													}
												}
											});
										});
									};
									output.on("end", archive_finish);
									output.on("close", archive_finish);
									output.on("error", queue_next);
									output.on("warning", queue_next);
									archive.pipe(output);
									item_dirs.forEach(function(item_dir) {
										archive.directory(item_dir, false);
									});
									archive.finalize();
								} else {
									queue_next();
								}
							}
						}
					});
				});
			}
		});
	}
}
function register_download(video, file_ext, file) {
	if (file_ext === "mp4") {
		if (fallback_sd) {
			var sd_file = file_name(video, "mp4_sd", file_tag_default, description);
			fs.unlink(sd_file, function(error) {
				if (debug) console_log("test_file_delete: " + sd_file);
				if (error === null) {
					print_log("deleted: " + sd_file);
				} else if (description && video.desc.length > 0) {
					fs.unlink(sd_file = file_name(video, "mp4_sd", file_tag_default), function(error) {
						if (debug) console_log("test_file_delete: " + sd_file);
						if (error === null) {
							print_log("deleted: " + sd_file);
						}
					});
				}
			});
		}
		if (require_ffprobe) {
			var error_file = file_name(video, "mp4", file_tag_error, description);
			fs.unlink(error_file, function(error) {
				if (debug) console_log("test_file_delete: " + error_file);
				if (error === null) {
					print_log("deleted: " + error_file);
				} else if (description && video.desc.length > 0) {
					fs.unlink(error_file = file_name(video, "mp4", file_tag_error), function(error) {
						if (debug) console_log("test_file_delete: " + error_file);
						if (error === null) {
							print_log("deleted: " + error_file);
						}
					});
				}
			});
		}
		if (alert) {
			try {
				child_process.spawn(alert_proc, [
//					alert_args.replaceAll("{}", file)
					alert_args.replaceAll("{}", file_path_absolute(file))
				], {stdio: "ignore", detached: true}).unref();
			} catch (e) {
				console_error(e);
			}
		}
	}
}
function downloadVideosMulti(ids, callback) {
	fetched_ids = [...new Set(ids)];
	ids = fetched_ids.filter(id => !ignore_ids.has(id));
	var a = 0, i = 0, e = 0, y = 0, c = 0, m = ids.length > 0 && !fetch_only, v = [], cursor_printed, cursor_start, cursor_end, f0 = function() {
		if (running) {
			while ((download_files || metadata_save_each) && i < v.length && e < max_downloads) {
				download_item(v[i++], f1);
			}
			if (m && e < max_downloads) {
				m = false, a++;
				print_cursor();
				requestAweme(aweme_host_details, "/aweme/v1/multi/aweme/detail/?aweme_ids=[" + ids.slice(c, c + count).join() + "]", f2);//+ "origin_type="//+ "push_params="
			}
		}
		if (a === 0 && e === 0) {
			a--;
			print_cursor();
			save_to_files();
			callback(null);
		}
	}, print_cursor = function() {
		if (cursor_print && cursor_printed !== [c,y]) {
			cursor_printed = [c,y];
			console_log("cursor: " + c + "/" + ids.length);
			console_log("item count: " + y + (limit > 0 ? "/" + limit + (break_limit ? "+" : "") : ""));
			if (cursor_times) {
				cursor_end = Date.now();
				if (c !== cursor) console_log("duration: " + ((cursor_end-cursor_start)/1000).toFixed(3) + "s");
				cursor_start = cursor_end;
			}
		}
	}, f1 = function(video, file_ext) {
		e++;
		ensureAweme(video, file_ext, function(error, file, exists) {
			if (error !== null) {
				if (debug || error !== reject_sd_error) print_log("failed: " + file + " | " + error.message);
				if (fallback_sd && file_ext === "mp4" && video["mp4_sd"] !== null) {
					f1(video, "mp4_sd");
				}
			} else {
				if (file_ext === "mp4") {
					ignore_ids.add(video.id);
				}
				if (!exists) {
					print_log("downloaded: " + file);
					register_download(video, file_ext, file);
				}
			}
			e--;
			f0();
		});
	}, f2 = function(error, data) {
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				m = true;
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
			} else {
				console_error(error);
			}
		} else {
			c = Math.min(c+count, ids.length),
			m = c < ids.length,
			ignore_errors_count = 0;
			if (typeof data === "object" && data !== null
			&& typeof data.aweme_details === "object" && data.aweme_details !== null) {
				for (var j = 0, u; j < data.aweme_details.length; j++) {
					if (typeof data.aweme_details[j].video === "object" && data.aweme_details[j].video !== null
					&& (u = getVideoUrlList(data.aweme_details[j].video)) !== null) {
						var video = video_format(data.aweme_details[j], u, data.extra.now);
						if (video === null) continue;
						if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
						if (metadata_save) metadata.push(video.metadata);
						v.push(video);
						if (++y === limit) {
							m = false;
							if (!break_limit) break;
						}
					}
				}
			}
		}
		a--;
		f0();
	};
	if (download_videos_tikwm) {
		print_cursor();
		m = false,
		c = ids.length,
		y = ids.length,
		v = ids.map(id => video_format_tikwm(id));
	}
	f0();
}
function downloadVideos(ids, callback) {
	fetched_ids = [...new Set(ids)];
	ids = fetched_ids.filter(id => !ignore_ids.has(id));
	var a = 0, i = 0, e = 0, y = 0, c = 0, m = ids.length > 0 && !fetch_only, v = [], error_queue = [], cursor_printed, cursor_start, cursor_end, f0 = function() {
		if (running) {
			while ((download_files || metadata_save_each) && i < v.length && e < max_downloads) {
				download_item(v[i++], f1);
			}
			while (m && a < max_downloads && error_queue.length) {
				a++;
				requestAweme(aweme_host_details, error_queue.shift(), f2);
			}
			while (m && a < max_downloads && c < ids.length) {
				a++;
				if (c % max_downloads === 0) print_cursor();
				requestAweme(aweme_host_details, "/aweme/v1/aweme/detail/?aweme_id=" + ids[c++], f2);
			}
		}
		if (a === 0 && e === 0) {
			a--;
			print_cursor();
			save_to_files();
			callback(null);
		}
	}, print_cursor = function() {
		if (cursor_print && cursor_printed !== [c,y]) {
			cursor_printed = [c,y];
			console_log("cursor: " + c + "/" + ids.length);
			console_log("item count: " + y + (limit > 0 ? "/" + limit + (break_limit ? "+" : "") : ""));
			if (cursor_times) {
				cursor_end = Date.now();
				if (c !== cursor) console_log("duration: " + ((cursor_end-cursor_start)/1000).toFixed(3) + "s");
				cursor_start = cursor_end;
			}
		}
	}, f1 = function(video, file_ext) {
		e++;
		ensureAweme(video, file_ext, function(error, file, exists) {
			if (error !== null) {
				if (debug || error !== reject_sd_error) print_log("failed: " + file + " | " + error.message);
				if (fallback_sd && file_ext === "mp4" && video["mp4_sd"] !== null) {
					f1(video, "mp4_sd");
				}
			} else {
				if (file_ext === "mp4") {
					ignore_ids.add(video.id);
				}
				if (!exists) {
					print_log("downloaded: " + file);
					register_download(video, file_ext, file);
				}
			}
			e--;
			f0();
		});
	}, f2 = function(error, data, uri) {
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
				error_queue.push(uri);
			} else {
				m = false;
				console_error(error);
			}
		} else if (m || break_limit) {
			ignore_errors_count = 0;
			var u, video;
			if (typeof data === "object" && data !== null
			&& typeof data.aweme_detail === "object" && data.aweme_detail !== null
			&& typeof data.aweme_detail.video === "object" && data.aweme_detail.video !== null
			&& (u = getVideoUrlList(data.aweme_detail.video)) !== null
			&& (video = video_format(data.aweme_detail, u, data.extra.now)) !== null) {
				if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
				if (metadata_save) metadata.push(video.metadata);
				v.push(video);
				if (++y === limit) m = false;
			}
		}
		a--;
		f0();
	};
	if (download_videos_tikwm) {
		print_cursor();
		m = false,
		c = ids.length,
		y = ids.length,
		v = ids.map(id => video_format_tikwm(id));
	}
	f0();
}
function downloadUserVideosWeb(sec_uid, callback) {
	var a = 0, i = 0, e = 0, y = 0, c = cursor, m = true, n, d, ds = [], v = [], cursor_printed, cursor_start, cursor_end, delay = 0, depth = 0, f0 = function() {
		if (running) {
			while ((download_files || metadata_save_each) && i < v.length && e < max_downloads) {
				download_item(v[i++], f1);
			}
			if (m && e < max_downloads) {
				m = false, a++;
				print_cursor();
				setTimeout(requestTiktok, delay, "/api/post/item_list/"
				+ "?aid=1988"
				+ "&secUid=" + sec_uid
//				+ "&id=" // ?
//				+ "&userId=" // ?
				+ "&count=" + count
				+ "&cursor=" + c, f2);
			}
		}
		if (a === 0 && e === 0) {
			a--;
			print_cursor();
			save_to_files();
			callback(null);
		}
	}, print_cursor = function() {
		if (cursor_print && cursor_printed !== [c,y]) {
			cursor_printed = [c,y];
			console_log("cursor: " + c + (c !== 0 ? " (" + (new Date(+c)).toISOString() + ") [" + (count*depth) + "=" + count + "*" + depth + (max_depth ? "/" + max_depth : "") + "]" : ""));
			console_log("item count: " + y + (!date_limit && limit > 0 ? "/" + limit + (break_limit ? "+" : "") : ""));
			if (cursor_times) {
				cursor_end = Date.now();
				if (c !== cursor) console_log("duration: " + ((cursor_end-cursor_start)/1000).toFixed(3) + "s");
				cursor_start = cursor_end;
			}
		}
	}, f1 = function(video, file_ext) {
		e++;
		ensureAweme(video, file_ext, function(error, file, exists) {
			if (error !== null) {
				last_scrape_save = false;
				if (debug || error !== reject_sd_error) print_log("failed: " + file + " | " + error.message);
				if (fallback_sd && file_ext === "mp4" && video["mp4_sd"] !== null) {
					f1(video, "mp4_sd");
				}
			} else {
				if (file_ext === "mp4") {
					ignore_ids.add(video.id);
				}
				if (!exists) {
					print_log("downloaded: " + file);
					register_download(video, file_ext, file);
				}
			}
			e--;
			f0();
		});
	}, f2 = function(error, data) {
		delay = 0;
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				delay = ignore_errors_delay,
				m = true;
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
			} else {
				last_scrape_save = false;
				console_error(error);
			}
		} else if (typeof data !== "object" || data === null
		|| (typeof data.cursor !== "string" && typeof data.cursor !== "number")
		|| typeof data.hasMore !== "boolean") {
			last_scrape_save = false;
			console_error(new Error("Unexpected response."));
			console_error(data);
		} else {
			n = data.hasMore,
			c = data.cursor,
			ignore_errors_count = 0;
			if (++depth === max_depth) {
				n = false;
			}
			if (typeof data.itemList === "object" && data.itemList !== null) {
				for (var j = 0, r = []; j < data.itemList.length; j++) {
					if (typeof data.itemList[j] === "object" && data.itemList[j] !== null) {
						var video = video_format_feed(data.itemList[j]);
						if (last_scrape_save && last_scrape === 0) {
							last_scrape = video.time;
						}
						if (date_limit && video.time <= limit) {
							if (debug) console_log("breaking at: " + video.time + " = " + createTimeToFilename(video.time) + " = " + video.id);
							limit = 0,
							n = false;
							if (!break_limit) break;
						}
						if (ignore_ids.has(video.id)) console_log("skipping: " + video.id);
						else if (download_videos_tikwm) {
							video["mp4_sd"] = getUriApi(video.id);
							if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
							if (metadata_save) metadata.push(video.metadata);
							v.push(video);
						}
						else if (download_videos_single) f4(video.id);
						else r.push(video.id);
						if (last_scrape_save || fetched_ids_save || unscraped_ids_save) fetched_ids.push(video.id);
						if (++y === limit && !date_limit) {
							n = false;
							if (!break_limit) break;
						}
					}
				}
				if (!fetch_only && r.length > 0) {
					requestAweme(aweme_host_details, d = "/aweme/v1/multi/aweme/detail/?aweme_ids=[" + r.join(",") + "]", f3);
					return;
				}
			}
			m = n;
		}
		a--;
		f0();
	}, f3 = function(error, data) {
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1;
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
				setTimeout(requestAweme, ignore_errors_delay, aweme_host_details, d, f3);
				return;
			} else {
				last_scrape_save = false;
				console_error(error);
			}
		} else {
			ignore_errors_count = 0;
			m = n;
			if (typeof data === "object" && data !== null
			&& typeof data.aweme_details === "object" && data.aweme_details !== null) {
				for (var j = 0, u; j < data.aweme_details.length; j++) {
					if (typeof data.aweme_details[j].video === "object" && data.aweme_details[j].video !== null
					&& (u = getVideoUrlList(data.aweme_details[j].video)) !== null) {
						var video = video_format(data.aweme_details[j], u, data.extra.now);
						if (video === null) continue;
						if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
						if (metadata_save) metadata.push(video.metadata);
						v.push(video);
					}
				}
			}
			if (last_scrape_save && v.length !== fetched_ids.length) {
				last_scrape_save = false;
			}
		}
		a--;
		f0();
	}, f4 = function(id) {
		if (a >= max_downloads) {
			ds.push(arguments);
		} else {
			a++;
			var queue_next = function() {
				a--;
				if (ds.length > 0) {
					f4(...ds.shift());
				}
			};
			requestAweme(aweme_host_details, "/aweme/v1/aweme/detail/?aweme_id=" + id, function(error, data) {
				if (error !== null) {
					if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
						ignore_errors_count += 1;
						console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
						f4(id);
					} else {
						last_scrape_save = false;
						console_error(error);
					}
				} else {
					ignore_errors_count = 0;
					var u, video;
					if (typeof data === "object" && data !== null
					&& typeof data.aweme_detail === "object" && data.aweme_detail !== null
					&& typeof data.aweme_detail.video === "object" && data.aweme_detail.video !== null
					&& (u = getVideoUrlList(data.aweme_detail.video)) !== null
					&& (video = video_format(data.aweme_detail, u, data.extra.now)) !== null) {
						if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
						if (metadata_save) metadata.push(video.metadata);
						v.push(video);
					}
				}
				queue_next();
				f0();
			});
		}
	};
	f0();
}
function downloadUserVideosWebPage(id, callback) {
	requestGet("https://www.tiktok.com/@" + id, {}, function(error, response, request) {
		var d = "";
		response.setEncoding("utf8");
		response.on("data", function(data) {
			d += data;
		}).on("error", function(error) {
			console_error(error);
		}).on("end", function() {
			var m = d.match(/,"list":(\[[\d",]+\])/);
			if (m) {
				var download_videos_func = download_videos_single ? downloadVideos : downloadVideosMulti;
				download_videos_func(JSON.parse(m.pop()), emptyFunction);
			}
		});
	});
}
function scrapeFeed(feed_type, id, callback) {
	var a = 0, i = 0, e = 0, y = 0, c = cursor, m = true, n, d, ds = [], v = [], cursor_printed, cursor_start, cursor_end, delay = 0, depth = 0, f0 = function() {
		if (running) {
			while ((download_files || metadata_save_each) && i < v.length && e < max_downloads) {
				download_item(v[i++], f1);
			}
			if (m && e < max_downloads) {
				m = false, a++;
				print_cursor();
				setTimeout(requestTiktok, delay, "/node/video/feed"
				+ "?secUid="
				+ "&id=" + id
				+ "&type=" + feed_type
				+ "&count=" + count
				+ "&minCursor=0"
				+ "&maxCursor=" + c
				+ "&shareUid="
				+ "&lang=en", f2);
			}
		}
		if (a === 0 && e === 0) {
			a--;
			print_cursor();
			save_to_files();
			callback(null);
		}
	}, print_cursor = function() {
		if (cursor_print && cursor_printed !== [c,y]) {
			cursor_printed = [c,y];
			console_log("cursor: " + c + (c !== 0 ? (feed_type === 1 ? " (" + (new Date(+c)).toISOString() + ")" : "") + " [" + (count*depth) + "=" + count + "*" + depth + (max_depth ? "/" + max_depth : "") + "]" : ""));
			console_log("item count: " + y + (!date_limit && limit > 0 ? "/" + limit + (break_limit ? "+" : "") : ""));
			if (cursor_times) {
				cursor_end = Date.now();
				if (c !== cursor) console_log("duration: " + ((cursor_end-cursor_start)/1000).toFixed(3) + "s");
				cursor_start = cursor_end;
			}
		}
	}, f1 = function(video, file_ext) {
		e++;
		ensureAweme(video, file_ext, function(error, file, exists) {
			if (error !== null) {
				last_scrape_save = false;
				if (debug || error !== reject_sd_error) print_log("failed: " + file + " | " + error.message);
				if (fallback_sd && file_ext === "mp4" && video["mp4_sd"] !== null) {
					f1(video, "mp4_sd");
				}
			} else {
				if (file_ext === "mp4") {
					ignore_ids.add(video.id);
				}
				if (!exists) {
					print_log("downloaded: " + file);
					register_download(video, file_ext, file);
				}
			}
			e--;
			f0();
		});
	}, f2 = function(error, data) {
		delay = 0;
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				delay = ignore_errors_delay,
				m = true;
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
			} else {
				last_scrape_save = false;
				console_error(error);
			}
		} else if (typeof data !== "object" || data === null
		|| typeof data.body !== "object" || data.body === null
		|| (typeof data.body.maxCursor !== "string" && typeof data.body.maxCursor !== "number")
		|| typeof data.body.hasMore !== "boolean") {
			last_scrape_save = false;
			console_error(new Error("Unexpected response."));
			console_error(data);
		} else {
			n = data.body.hasMore,
			c = data.body.maxCursor,
			ignore_errors_count = 0;
			if (++depth === max_depth) {
				n = false;
			}
			if (typeof data.body.itemListData === "object" && data.body.itemListData !== null) {
				for (var j = 0, r = []; j < data.body.itemListData.length; j++) {
					if (typeof data.body.itemListData[j].itemInfos === "object" && data.body.itemListData[j].itemInfos !== null) {
						var video = video_format_feed(data.body.itemListData[j].itemInfos);
						if (last_scrape_save && last_scrape === 0) {
							last_scrape = video.time;
						}
						if (date_limit && video.time <= limit) {
							if (debug) console_log("breaking at: " + video.time + " = " + createTimeToFilename(video.time) + " = " + video.id);
							limit = 0,
							n = false;
							if (!break_limit) break;
						}
						if (ignore_ids.has(video.id)) console_log("skipping: " + video.id);
						else if (download_videos_tikwm) {
							video["mp4_sd"] = getUriApi(video.id);
							if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
							if (metadata_save) metadata.push(video.metadata);
							v.push(video);
						}
						else if (download_videos_single) f4(video.id);
						else r.push(video.id);
						if (last_scrape_save || fetched_ids_save || unscraped_ids_save) fetched_ids.push(video.id);
						if (++y === limit && !date_limit) {
							n = false;
							if (!break_limit) break;
						}
					}
				}
				if (!fetch_only && r.length > 0) {
					requestAweme(aweme_host_details, d = "/aweme/v1/multi/aweme/detail/?aweme_ids=[" + r.join(",") + "]", f3);
					return;
				}
			}
			m = n;
		}
		a--;
		f0();
	}, f3 = function(error, data) {
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1;
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
				setTimeout(requestAweme, ignore_errors_delay, aweme_host_details, d, f3);
				return;
			} else {
				last_scrape_save = false;
				console_error(error);
			}
		} else {
			ignore_errors_count = 0;
			m = n;
			if (typeof data === "object" && data !== null
			&& typeof data.aweme_details === "object" && data.aweme_details !== null) {
				for (var j = 0, u; j < data.aweme_details.length; j++) {
					if (typeof data.aweme_details[j].video === "object" && data.aweme_details[j].video !== null
					&& (u = getVideoUrlList(data.aweme_details[j].video)) !== null) {
						var video = video_format(data.aweme_details[j], u, data.extra.now);
						if (video === null) continue;
						if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
						if (metadata_save) metadata.push(video.metadata);
						v.push(video);
					}
				}
			}
			if (last_scrape_save && v.length !== fetched_ids.length) {
				last_scrape_save = false;
			}
		}
		a--;
		f0();
	}, f4 = function(id) {
		if (a >= max_downloads) {
			ds.push(arguments);
		} else {
			a++;
			var queue_next = function() {
				a--;
				if (ds.length > 0) {
					f4(...ds.shift());
				}
			};
			requestAweme(aweme_host_details, "/aweme/v1/aweme/detail/?aweme_id=" + id, function(error, data) {
				if (error !== null) {
					if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
						ignore_errors_count += 1;
						console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
						f4(id);
					} else {
						last_scrape_save = false;
						console_error(error);
					}
				} else {
					ignore_errors_count = 0;
					var u, video;
					if (typeof data === "object" && data !== null
					&& typeof data.aweme_detail === "object" && data.aweme_detail !== null
					&& typeof data.aweme_detail.video === "object" && data.aweme_detail.video !== null
					&& (u = getVideoUrlList(data.aweme_detail.video)) !== null
					&& (video = video_format(data.aweme_detail, u, data.extra.now)) !== null) {
						if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
						if (metadata_save) metadata.push(video.metadata);
						v.push(video);
					}
				}
				queue_next();
				f0();
			});
		}
	};
	f0();
}
function scrapeAweme(o, callback, handle_metadata=true) {
	var a = 0, i = 0, e = 0, y = 0, c = cursor, m = true, n, v = [], cursor_printed, cursor_start, cursor_end, delay = 0, depth = 0, f0 = function() {
		if (running) {
			while ((download_files || metadata_save_each) && i < v.length && e < max_downloads) {
				download_item(v[i++], f1);
			}
			if (m && e < max_downloads) {
				m = false, a++;
				print_cursor();
				setTimeout(requestAweme, delay, o.host, o.path + c, f2);
			}
		}
		if (a === 0 && e === 0) {
			a--;
			print_cursor();
			if (handle_metadata) save_to_files();
			callback(null);
		}
	}, print_cursor = function() {
		if (cursor_print && cursor_printed !== [c,y]) {
			cursor_printed = [c,y];
			console_log((handle_metadata ? "" : "\t") + "cursor: " + c + (c !== 0 ? (o.date_cursor ? " (" + (new Date(+c)).toISOString() + ")" : "") + " [" + (count*depth) + "=" + count + "*" + depth + (max_depth ? "/" + max_depth : "") + "]" : ""));
			console_log((handle_metadata ? "" : "\t") + "item count: " + y + (!date_limit && limit > 0 ? "/" + limit + (break_limit ? "+" : "") : ""));
			if (cursor_times) {
				cursor_end = Date.now();
				if (c !== cursor) console_log((handle_metadata ? "" : "\t") + "duration: " + ((cursor_end-cursor_start)/1000).toFixed(3) + "s");
				cursor_start = cursor_end;
			}
		}
	}, f1 = function(video, file_ext) {
		e++;
		ensureAweme(video, file_ext, function(error, file, exists) {
			if (error !== null) {
				last_scrape_save = false;
				if (debug || error !== reject_sd_error) print_log("failed: " + file + " | " + error.message);
				if (fallback_sd && file_ext === "mp4" && video["mp4_sd"] !== null) {
					f1(video, "mp4_sd");
				}
			} else {
				if (file_ext === "mp4") {
					ignore_ids.add(video.id);
				}
				if (!exists) {
					if (scraped_ids_save || unscraped_ids_save) scraped_ids.push(video.id);
					print_log("downloaded: " + file);
					register_download(video, file_ext, file);
				}
			}
			e--;
			f0();
		});
	}, f2 = function(error, data) {
		delay = 0;
		var details = get_nested(data, o.details);
		var detail = get_nested(details, o.detail);
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				delay = ignore_errors_delay,
				m = true;
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
			} else {
				last_scrape_save = false;
				console_error(error);
			}
		} else if (data.status_code !== 0) { // success
			console_error(awemeError(data));
		} else if (typeof detail !== "object" || detail === null) {
		} else if (typeof details !== "object" || details === null
		|| (typeof details[o.max_cursor] !== "string" && typeof details[o.max_cursor] !== "number")
		|| (typeof details[o.has_more] !== "boolean" && typeof details[o.has_more] !== "number")) {
			last_scrape_save = false;
			console_error(new Error("Unexpected response."));
			console_error(data);
		} else {
			n = details[o.has_more],
			c = details[o.max_cursor],
			ignore_errors_count = 0;
			if (++depth === max_depth) {
				n = false;
			}
			if (typeof detail === "object" && detail !== null) {
				for (var j = 0, u; j < detail.length; j++) {
					var aweme_detail = detail[j];
					if (o.item_detail && aweme_detail.hasOwnProperty(o.item_detail)) {
						aweme_detail = aweme_detail[o.item_detail];
					}
					if (o.dump_metadata) {
						if (o.dump_metadata === true || metadata_verbose) {
							metadata.push(aweme_detail);
						} else {
							metadata.push(obj_filter(aweme_detail, o.dump_metadata));
						}
					} else if (typeof aweme_detail.video === "object" && aweme_detail.video !== null
					&& (u = getVideoUrlList(aweme_detail.video)) !== null) {
						var video = video_format(aweme_detail, u, data.extra.now);
						if (video === null) continue;
						if (last_scrape_save && last_scrape === 0 && !data.aweme_list[j].is_top) {
							last_scrape = video.time;
						}
						if (date_limit && video.time <= limit && !data.aweme_list[j].is_top) {
							if (debug) console_log("breaking at: " + video.time + " = " + createTimeToFilename(video.time) + " = " + video.id);
							limit = 0,
							n = false;
							if (!break_limit) break;
						}
						if (metadata_save && metadata_save_ignored) metadata.push(video.metadata);
						if (!ignore_ids.has(video.id)) {
							if (metadata_save && !metadata_save_ignored) metadata.push(video.metadata);
							v.push(video);
						} else if (debug) {
							console_log("skipping: " + video.id);
						}
						if (fetched_ids_save || unscraped_ids_save) fetched_ids.push(video.id);
					}
					if (++y === limit && !date_limit) {
						n = false;
						if (!break_limit) break;
					}
				}
			}
			m = n;
		}
		a--;
		f0();
	};
	f0();
}
function scrapeStories(callback) {
	var i = 0, e = 0, v = [], f0 = function() {
		if (running) {
			while (i < v.length && e < max_downloads) {
				e++;
				downloadUserStories(v[i++], function() {
					e--;
					f0();
				}, false);
			}
		}
		if (e === 0) {
			save_to_files();
			callback(null);
		}
	}, f1 = function(error, data) {
		if (error !== null) {
			console_error(error);
		} else if (data.status_code !== 0) { // success
			console_error(awemeError(data));
		} else if (typeof data.user_ids === "object" && data.user_ids !== null) {
			v = data.user_ids;
			if (cursor_print) {
				console_log("active story pages: " + v.length);
			}
		}
		f0();
	};
	requestAweme(aweme_host_stories, "/tiktok/v1/story/get_feed?refresh=true", f1);
}
function scrapeCollections(callback) {
	var a = 0, i = 0, e = 0, y = 0, c = cursor, m = true, n, v = [], cursor_printed, cursor_start, cursor_end, delay = 0, depth = 0, o = {
		host: aweme_host_collections,
		path: "/tiktok/collection/item_archive/list/v1"
		+ "?count=" + count
		+ "&cursor=",
		details: [],
		detail: "item_archive_detail_list",
		max_cursor: "cursor",
		has_more: "has_more"
	}, f0 = function() {
		if (running) {
			while (i < v.length && e < max_downloads) {
				e++;
				if (cursor_print) console_log("collection " + v[i].id + ": " + v[i].name + " [" + v[i].statistic.total + "]");
				downloadCollection(v[i++].id, function() {
					e--;
					f0();
				}, false);
			}
			if (m && e < max_downloads) {
				m = false, a++;
				print_cursor();
				setTimeout(requestAweme, delay, o.host, o.path + c, f2);
			}
		}
		if (a === 0 && e === 0) {
			a--;
			print_cursor();
			save_to_files();
			callback(null);
		}
	}, print_cursor = function() {
		if (cursor_print && cursor_printed !== [c,y]) {
			cursor_printed = [c,y];
			console_log("cursor: " + c);
			console_log("item count: " + y);
			if (cursor_times) {
				cursor_end = Date.now();
				if (c !== cursor) console_log("duration: " + ((cursor_end-cursor_start)/1000).toFixed(3) + "s");
				cursor_start = cursor_end;
			}
		}
	}, f2 = function(error, data) {
		delay = 0;
		var details = get_nested(data, o.details);
		var detail = get_nested(details, o.detail);
		if (error !== null) {
			if (running && ignore_errors && (ignore_errors_limit === 0 || ignore_errors_count < ignore_errors_limit)) {
				ignore_errors_count += 1,
				delay = ignore_errors_delay,
				m = true;
				console_error("ignoring: " + error.toString() + " | " + ignore_errors_count + (ignore_errors_limit !== 0 ? " / " + ignore_errors_limit : ""));
			} else {
				last_scrape_save = false;
				console_error(error);
			}
		} else if (data.status_code !== 0) { // success
			console_error(awemeError(data));
		} else if (typeof detail !== "object" || detail === null) {
		} else if (typeof details !== "object" || details === null
		|| (typeof details[o.max_cursor] !== "string" && typeof details[o.max_cursor] !== "number")
		|| (typeof details[o.has_more] !== "boolean" && typeof details[o.has_more] !== "number")) {
			console_error(new Error("Unexpected response."));
			console_error(data);
		} else {
			n = details[o.has_more],
			c = details[o.max_cursor],
			ignore_errors_count = 0;
			if (typeof detail === "object" && detail !== null) {
				for (var j = 0; j < detail.length; j++, y++) {
					if (typeof detail[j] === "object" && detail[j] !== null) {
						v.push(detail[j]);
					}
				}
			}
			m = n;
		}
		a--;
		f0();
	};
	f0();
}
function get_nested(o, ...ks) {
	return ks.flat().reduce((v,k) => (typeof v === "object" && v !== null && typeof v[k] === "object" && v[k] !== null) ? v[k] : null, o);
}
function downloadHashtagVideosAweme(id, ...a) {
		scrapeAweme({
		host: aweme_host_tags,
		path: "/aweme/v1/challenge/aweme/"
		+ "?ch_id=" + id
		+ "&query_type=0"
		+ "&source=challenge_video"
		+ "&type=5"
		+ "&count=" + count
		+ "&cursor=",
		details: [],
		detail: "aweme_list",
		max_cursor: "cursor",
		has_more: "has_more"
	}, ...a, emptyFunction);
}
function downloadMusicVideosAweme(id, ...a) {
		scrapeAweme({
		host: aweme_host_music,
		path: "/aweme/v1/music/aweme/"
		+ "?music_id=" + id
		+ "&type=0"
		+ "&count=" + count
		+ "&cursor=",
		details: [],
		detail: "aweme_list",
		max_cursor: "cursor",
		has_more: "has_more"
	}, ...a, emptyFunction);
}
function downloadUserVideosAweme(id, ...a) {
		scrapeAweme({
		host: aweme_host_posts,
		path: "/aweme/v1/aweme/post/"
		+ "?" + (sec_uid ? "sec_user_id=" + sec_uid : "user_id=" + id)
		+ "&source=0"
		+ "&count=" + count
		+ "&max_cursor=",
		details: [],
		detail: "aweme_list",
		max_cursor: "max_cursor",
		has_more: "has_more",
		date_cursor: true
	}, ...a, emptyFunction);
}
function downloadUserLikesAweme(id, ...a) {
		scrapeAweme({
		host: aweme_host_likes,
		path: "/aweme/v1/aweme/favorite/"
		+ "?" + (sec_uid ? "sec_user_id=" + sec_uid : "user_id=" + id)
		+ "&invalid_item_count=0"
		+ "&is_hiding_invalid_item=0"
		+ "&count=" + count
		+ "&max_cursor=",
		details: [],
		detail: "aweme_list",
		max_cursor: "max_cursor",
		has_more: "has_more",
		date_cursor: true
	}, ...a, emptyFunction);
}
function downloadUserStories(id, ...a) {
	if (cursor === 0) cursor = 1;
	scrapeAweme({
		host: aweme_host_stories,
		path: "/tiktok/v1/story/get_user_story"
		+ "?author_id=" + id
		+ "&count=" + count
		+ "&cursor=",
		details: "user_story",
		detail: "stories",
		max_cursor: "max_cursor",
		has_more: "has_more_after",
		date_cursor: true
	}, ...a);
}
function downloadTrendingAweme(...a) {
	scrapeAweme({
		host: aweme_host_trending,
		path: "/aweme/v1/feed/"
		+ "?offset=0"
		+ "&type=0"
		+ "&is_cold_start=1"
		+ "&pull_type=1"
		+ "&count=" + count
		+ "&max_cursor=",
		details: [],
		detail: "aweme_list",
		max_cursor: "max_cursor",
		has_more: "has_more"
	}, ...a);
}
function downloadFollowedAweme(...a) {
	scrapeAweme({
		host: aweme_host_followed,
		path: "/aweme/v2/follow/feed/"
		+ "?count=" + count
		+ "&cursor=",
		details: [],
		detail: "data",
		item_detail: "aweme",
		max_cursor: "cursor",
		has_more: "has_more"
	}, ...a);
}
function dumpVideoComments(id, ...a) {
	scrapeAweme({
		host: aweme_host_comments,
		path: "/aweme/v2/comment/list/"
		+ "?aweme_id=" + id
		+ "&count=" + count
		+ "&cursor=",
		details: [],
		detail: "comments",
		max_cursor: "cursor",
		has_more: "has_more",
		dump_metadata: commentMetadataFilter()
	}, ...a);
}
function dumpVideoCommentReplies(id, ...a) {
	scrapeAweme({
		host: aweme_host_comments,
		path: "/aweme/v1/comment/list/reply/"
		+ "?comment_id=" + id
		+ "&count=" + count
		+ "&cursor=",
		details: [],
		detail: "comments",
		max_cursor: "cursor",
		has_more: "has_more",
		dump_metadata: commentMetadataFilter()
	}, ...a);
}
function commentMetadataFilter() {
	return {
		aweme_id: "",
		cid: "",
		reply_id: "",
		reply_to_reply_id: "",
		reply_to_userid: "",
		reply_to_username: "",
		create_time: 0,
		reply_comment_total: 0,
		digg_count: 0,
		is_author_digged: 0,
		author_pin: 0,
		text: "",
		text_extra: true,
		user: {
			uid: "",
			sec_uid: "",
			unique_id: "",
			nickname: ""
		}
	}
}
function dumpCollectionsMetadata(...a) {
	scrapeAweme({
		host: aweme_host_collections,
		path: "/tiktok/collection/item_archive/list/v1"
		+ "?count=" + count
		+ "&cursor=",
		details: [],
		detail: "item_archive_detail_list",
		max_cursor: "cursor",
		has_more: "has_more",
		dump_metadata: {
			id: "",
			name: "",
			status: true,
			statistic: true
		}
	}, ...a);
}
function downloadCollection(id, ...a) {
	scrapeAweme({
		host: aweme_host_collections,
		path: "/tiktok/collection/item_archive/item/list/v1"
		+ "?item_archive_id=" + id
		+ "&count=" + count
		+ "&cursor=",
		details: [],
		detail: "items",
		max_cursor: "max_cursor",
		has_more: "has_more"
	}, ...a);
}
function downloadUserVideos(idOrName, callback) {
	if (idOrName.charCodeAt(0) === 64) {
		requestTiktok("/node/share/user/@" + encodeURI(idOrName.substring(1)) + "?user_agent=", function(error, data) {
			if (error !== null) {
				console_error(error);
			} else if (typeof data !== "object" || data === null
			|| typeof data.userInfo !== "object" || data.userInfo === null
			|| typeof data.userInfo.user !== "object" || typeof data.userInfo.user === null) {
				console_error(new Error("Unexpected response."));
			} else {
//				scrapeFeed(1, data.userInfo.user.id, callback);
				downloadUserVideosAweme(data.userInfo.user.id, callback);
			}
		});
	} else {
//		scrapeFeed(1, idOrName, callback);
		downloadUserVideosAweme(idOrName, callback);
	}
}
function downloadUserLikes(idOrName, callback) {
	if (idOrName.charCodeAt(0) === 64) {
		requestTiktok("/node/share/user/@" + encodeURI(idOrName.substring(1)) + "?user_agent=", function(error, data) {
			if (error !== null) {
				console_error(error);
			} else if (typeof data !== "object" || data === null
			|| typeof data.userInfo !== "object" || data.userInfo === null
			|| typeof data.userInfo.user !== "object" || typeof data.userInfo.user === null) {
				console_error(new Error("Unexpected response."));
			} else {
//				scrapeFeed(2, data.userInfo.user.id, callback);
				downloadUserLikesAweme(data.userInfo.user.id, callback);
			}
		});
	} else {
//		scrapeFeed(2, idOrName, callback);
		downloadUserLikesAweme(idOrName, callback);
	}
}
function downloadHashtagVideos(idOrTag, callback) {
	if (idOrTag.charCodeAt(0) === 35) {
		requestTiktok("/node/share/tag/" + encodeURI(idOrTag.substring(1)) + "?user_agent=", function(error, data) {
			if (error !== null) {
				console_error(error);
			} else if (typeof data !== "object" || data === null
			|| typeof data.challengeInfo !== "object" || data.challengeInfo === null
			|| typeof data.challengeInfo.challenge !== "object" || typeof data.challengeInfo.challenge === null) {
				console_error(new Error("Unexpected response."));
			} else {
//				scrapeFeed(3, data.challengeInfo.challenge.id, callback);
				downloadHashtagVideosAweme(data.challengeInfo.challenge.id, callback);
			}
		});
	} else {
//		scrapeFeed(3, idOrTag, callback);
		downloadHashtagVideosAweme(idOrTag, callback);
	}
}
function downloadMusicVideos(id, callback) {
//	scrapeFeed(4, id, callback);
	downloadMusicVideosAweme(id, callback);
}
function downloadTrending(id, callback) {
//	scrapeFeed(5, id, callback);
	downloadTrendingAweme(id, callback);
}
function execCmds(commands, repeat, processes, output, use_shell, refresh_commands, callback) {
	if (output) {
		process.stdout.setMaxListeners(0);
		process.stderr.setMaxListeners(0);
	}
	var i = 0, e = 0, l = commands.length, pad_len = l.toString().length, l_str = l.toString().padStart(pad_len, "0"), f0 = function() {
		timeout = null;
		if (running) {
			while (i < l && e < processes) {
				f1(i);
				i++;
				e++;
			}
			if (e === 0 && i === l && repeat > -1) {
				i = 0;
				var d = Date.now();
				console_log([
					output_timestamp(new Date(d)),
					"sleeping",
					output_timestamp(new Date(d+repeat))
				].join(" | "));
				timeout = setTimeout(function() {
					if (refresh_commands) {
						if (commands_file !== undefined) {
							try {
								commands_file_cmds = fs.readFileSync(commands_file, {flag: "r", encoding: "utf8"});
							} catch (error) {
								console_error(error);
							}
						}
						commands = getCmds(commands_file_cmds);
						if (l !== commands.length) {
							l = commands.length, pad_len = l.toString().length, l_str = l.toString().padStart(pad_len, "0");
						}
					}
					f0();
				}, repeat);
				return;
			}
		}
		if (e === 0 && (!running || i === l)) {
			callback(null);
		}
	}, f1 = function(index) {
		var a, p;
		try {
			a = splitCmd(commands[index], [__filename]);
		} catch (e_) {
			console_log([
				output_timestamp(new Date()),
				(index+1).toString().padStart(pad_len, " ") + " / " + l_str,
				"0".toString().padStart(5, " "),
				commands[index]
			].join(" | "));
			console_error(e_);
			process.nextTick(f2, null, null);
			return;
		}
		p = child_process.spawn("node", a, {stdio: output ? ["ignore", "pipe", "pipe"] : "ignore", shell: use_shell});
		p.on("exit", f2);
		if (output) {
			p.stdout.pipe(process.stdout);
			p.stderr.pipe(process.stderr);
		}
		//p["x-index"] = i;
		console_log([
			output_timestamp(new Date()),
			(index+1).toString().padStart(pad_len, " ") + " / " + l_str,
			p.pid.toString().padStart(5, " "),
			commands[index]
		].join(" | "));
	}, f2 = function(code, signal) {
		//console_log(this["x-index"] + " " + this.pid + " " + commands[this["x-index"]]);
		e--;
		f0();
	};
	f0();
}
function video_format(v, u, scrape_time) {
	if (skip_min_views && skip_min_views > v.statistics?.play_count) return null;
	if (skip_max_views && skip_max_views < v.statistics?.play_count) return null;

	var is_photo = typeof v.image_post_info === "object" && v.image_post_info !== null;
	if (skip_photos && is_photo) return null;
	if (only_photos && !is_photo) return null;

	var is_story = (typeof v.user_story === "object" && v.user_story !== null) || (typeof v.story_metadata === "object" && v.story_metadata !== null);
	if (skip_stories && is_story) return null;
	if (only_stories && !is_story) return null;

	var is_followed = typeof v.author?.follow_status === "number" && v.author.follow_status !== 0;
	if (skip_followed && is_followed) return null;
	if (only_followed && !is_followed) return null;

	var uid = v.author?.uid || "";
	if (skip_uids && skip_uids.has(uid)) return null;
	if (only_uids && !only_uids.has(uid)) return null;

	return {
		id: v.aweme_id,
		uid: uid,
		unique_id: v.author?.unique_id || "",
		time: +v.create_time || 0,
		desc: v.desc || "",
		mp4_wm: download_videos && !is_photo ? getVideoWatermarked(v.video) : null,
		mp4_sd: download_videos && !is_photo ? u[u.length - 1] : null,
//		mp4: download_videos && !is_photo ? getQualityUri(u[u.length - 1]) : null,
		mp4: download_videos && !is_photo ? getQualityUriApi(v.aweme_id) : null,
		jpg: download_thumbs ? getVideoThumbnail(v.video) : null,
		webp: download_thumbs_anim ? getVideoThumbnailAnimated(v.video) : null,
		mp3: download_videos && is_photo ? u[u.length - 1] : null,
		images: download_videos && is_photo ? getVideoImages(v) : null,
		is_photo: is_photo,
		is_story: is_story,
		is_mutual: v.status?.private_status === 2,
		is_private: v.author?.secret,
		metadata: metadata_print || metadata_save || metadata_save_each ? metadata_format(v, scrape_time) : null
	};
}
function video_format_feed(v) {
	var uri = null;
//	var uri = typeof v.video.bitrateInfo === "object" && v.video.bitrateInfo !== null ? v.video.bitrateInfo[0].PlayAddr.Uri : null;
//	console_log([v.id,uri,Boolean(uri)]);
	return {
		id: v.id,
		uid: v.authorId || "",
		unique_id: v.author?.uniqueId || "",
		time: +v.createTime || 0,
		desc: v.desc || "",
		mp4_wm: v.video.playAddr || null,
		mp4_sd: uri ? getUri(uri, aweme_host_details) : null,
//		mp4: uri ? getQualityUris(uri, aweme_host_details) : null,
		mp4: getQualityUriApi(v.id),
		jpg: v.video.cover || null,
		webp: v.video.dynamicCover || null,
		mp3: null,
		images: null,
		is_mutual: v.forFriend,
		is_private: v.author?.secret,
		metadata: metadata_print || metadata_save || metadata_save_each ? v : null
	};
}
function video_format_tikwm(id) {
	return {
		id: id,
		uid: "",
		unique_id: "",
		time: 0,
		desc: "",
		mp4_wm: getWatermarkedUriApi(id),
		mp4_sd: getUriApi(id),
		mp4: getQualityUriApi(id),
		jpg: null,
		webp: null,
		mp3: null,
		images: null,
		metadata: null
	};
}
function metadata_format(aweme_detail, scrape_time) {
	var ks = {};
	if (metadata_verbose) {
		ks = aweme_detail;
	} else {
		ks.aweme_id = aweme_detail.aweme_id;
		ks.create_time = aweme_detail.create_time;
		ks.desc = aweme_detail.desc;
		ks.uri = aweme_detail.video.play_addr.uri;
		if (typeof aweme_detail.text_extra === "object" && aweme_detail.text_extra !== null && aweme_detail.text_extra.length > 0) {
			var k = [];
			for (var i = 0; i < aweme_detail.text_extra.length; i++) {
				var v = aweme_detail.text_extra[i];
				v.text = aweme_detail.desc.substring(v.start, v.end);
				delete v.start;
				delete v.end;
				k.push(v);
			}
			ks.text_extra = k;
//			ks.text_extra = aweme_detail.text_extra;
		}
		if (typeof aweme_detail.interaction_stickers === "object" && aweme_detail.interaction_stickers !== null) {
			ks.interaction_stickers = aweme_detail.interaction_stickers;
		}
		if (typeof aweme_detail.interaction_tag_info === "object" && aweme_detail.interaction_tag_info !== null
		&& typeof aweme_detail.interaction_tag_info.tagged_users === "object" && aweme_detail.interaction_tag_info.tagged_users !== null
		&& aweme_detail.interaction_tag_info.tagged_users.length > 0) {
			var k = [];
			for (var i = 0; i < aweme_detail.interaction_tag_info.tagged_users.length; i++) {
				var v = aweme_detail.interaction_tag_info.tagged_users[i];
				k.push({
					nickname: v.nickname,
					uid: v.uid,
					unique_id: v.unique_id
				});
			}
			ks.interaction_tag_info = {"tagged_users": k};
		}
		if (typeof aweme_detail.statistics === "object" && aweme_detail.statistics !== null) {
			ks.statistics = aweme_detail.statistics;
			delete ks.statistics.aweme_id;
		}
		if (typeof aweme_detail.author === "object" && aweme_detail.author !== null) {
			var k = {};
			k.uid = aweme_detail.author.uid;
			k.sec_uid = aweme_detail.author.sec_uid;
			k.unique_id = aweme_detail.author.unique_id;
			k.nickname = aweme_detail.author.nickname;
			k.signature = aweme_detail.author.signature;
			var social_tags = ["ins_id","twitter_id","youtube_channel_id"];
			for (var i = 0; i < social_tags.length; i++) {
				if (aweme_detail.author[social_tags[i]] !== "" && aweme_detail.author[social_tags[i]] !== 0) {
					k[social_tags[i]] = aweme_detail.author[social_tags[i]];
				}
			}
			ks.author = k;
		}
		if (typeof aweme_detail.video === "object" && aweme_detail.video !== null) {
			var k = {};
			k.duration = aweme_detail.video.duration;
			k.width = aweme_detail.video.width;
			k.height = aweme_detail.video.height;
			ks.video = k;
		}
		if (typeof aweme_detail.music === "object" && aweme_detail.music !== null) {
			var k = {};
			k.duration = aweme_detail.music.duration;
			k.id_str = aweme_detail.music.id_str;
			k.owner_id = aweme_detail.music.owner_id;
			k.title = aweme_detail.music.title;
			k.author = aweme_detail.music.author;
			k.owner_handle = aweme_detail.music.owner_handle;
			k.owner_nickname = aweme_detail.music.owner_nickname;
			if (typeof aweme_detail.music.matched_song === "object" && aweme_detail.music.matched_song !== null) {
				var k2 = {};
				k2.id = aweme_detail.music.matched_song.id;
				k2.title = aweme_detail.music.matched_song.title;
				k2.author = aweme_detail.music.matched_song.author;
				k.matched_song = k2;
			}
			ks.music = k;
		}
	}
	ks.scrape_time = Math.floor(scrape_time / 1000);
	if (metadata_print) {
		console_log((metadata.length+1) + ": " + JSON.stringify(ks));
	}
	return ks;
}
function output_file_name(a, tag="", params=true) {
	var obj_out = [];
//	obj_out.push(path.basename(__filename, path.extname(__filename)));
	obj_out.push(path.basename(__filename));
	obj_out.push(createTimeToFilename(output_time/1000));
	if (params) {
		for (const [key, value] of Object.entries(output_tags.split(""))) {
			if (a[value] !== undefined) {
				if (a[value] === "") {
					obj_out.push(value);
				} else {
					obj_out.push([value,sanitizeFilename(a[value])].join("="));
				}
			}
		}
	}
	if (tag.length > 0) {
		obj_out.push(tag);
	}
	return obj_out.join("_") + ".json";
}
function save_to_files() {
	if (metadata_save) {
		metadata_save_to_file();
	}
	if (fetched_ids_save) {
		ids_save_to_file(fetched_ids, fetched_ids_file);
	}
	if (scraped_ids_save) {
		ids_save_to_file(scraped_ids, scraped_ids_file);
	}
	if (unscraped_ids_save) {
		for (var unscraped_ids = new Set(fetched_ids), j = 0; j < scraped_ids.length; j++) {
			unscraped_ids.delete(scraped_ids[j]);
		}
		ids_save_to_file([...unscraped_ids], unscraped_ids_file);
	}
	if (ignore_ids_save && ignore_ids.size !== ignore_ids_initial.size) {
		ids_save_to_file([...ignore_ids], ignore_ids_file);
	}
	if (last_scrape_save && last_scrape !== 0 && last_scrape !== previous_scrape) {
		if (ensureFileDirectorySync(last_scrape_file) !== null) return;
		fs.writeFileSync(last_scrape_file, last_scrape.toString(), {flag: "w", encoding: "utf8"});
		console_log("last scrape: " + last_scrape + " = " + createTimeToFilename(last_scrape) + " > " + last_scrape_file);
	}
}
function metadata_save_to_file() {
	if (save_empty || metadata.length > 0) {
		if (ensureFileDirectorySync(metadata_file) !== null) return;
		fs.writeFile(metadata_file, metadata_split ? metadata.map(ks => JSON.stringify(ks)).join("\n") : JSON.stringify(metadata), function(error) {
			if (error !== null) throw error;
			console_log("output file: " + file_path_absolute(metadata_file));
			console_log("item count: " + metadata.length);
		});
	}
}
function ids_save_to_file(ids, ids_file) {
	if (save_empty || ids.length > 0) {
		if (ensureFileDirectorySync(ids_file) !== null) return;
//		fs.writeFile(ids_file, JSON.stringify(ids.sort((a, b) => a - b)), function(error) {
		fs.writeFile(ids_file, JSON.stringify(ids), function(error) {
			if (error !== null) throw error;
			console_log("output file: " + file_path_absolute(ids_file));
			console_log("item count: " + ids.length);
		});
	}
}
function metadata_save_each_to_file(video) {
	ensureFile(video, add_description => file_name(video, "json", file_tag_default, add_description, false, true), function(error, file, exists) {
		if (error !== null) {
			console_error(error);
		} else if (!exists || metadata_update_each) {
			fs.writeFile(file, JSON.stringify(video.metadata), function(error) {
				if (error !== null) {
					console_error(error);
				} else {
					print_log((!exists ? "saved" : "updated") + " metadata: " + file);
				}
			});
		}
	});
}
function multi_progress_push_dummy() {
	try {
		multi_progress.bars.push("");
		multi_progress.move(multi_progress.bars.length - 1);
		multi_progress.stream.cursorTo(0);
		multi_progress.cursor += 1;
//		multi_progress.terminates += 1;
	} catch (e) {
		progress_bars = false;
		console_error(e);
	}
}
function output_timestamp(d) {
	return ("00" + d.getHours()).substr(-2)
	+ ":" + ("00" + d.getMinutes()).substr(-2)
	+ ":" + ("00" + d.getSeconds()).substr(-2)
	+ "." + ("000" + d.getMilliseconds()).substr(-3);
}
function output_line(v, n, k) {
	if (v && typeof v === "object" && v.constructor === Object) {
		try {
			v = JSON.stringify(v);
		} catch (e) {
			v = e;
		}
	}
	return (progress_bars_debug
		? output_timestamp(new Date()) + " "
		+ ("0000" + n).substr(-4) + " "
		+ k + " "
		: ""
	) + v;
}
function console_log(v) {
	print_log(v, false);
}
function console_error(v) {
	print_error(v);
}
function print_log(v, log=true) {
	if (progress_bars && multi_progress && process.stdout.isTTY) {
		multi_progress_push_dummy();
		console.log(output_line(v, multi_progress.cursor, "#"));
	} else {
		console.log(v);
	}
	if (log_save && (log || log_verbose)) {
		write_log(v);
	}
}
function print_error(v) {
	if (progress_bars && multi_progress && process.stderr.isTTY) {
		multi_progress_push_dummy();
		console.error(output_line(v, multi_progress.cursor, "!"));
	} else {
		console.error(v);
	}
	if (log_save && log_errors) {
		write_log(v);
	}
}
function write_log(v) {
	open_log(function(error) {
		if (error !== null) {
			log_save = false;
			console_error(error);
		} else if (v !== null) {
			if (typeof v === "object") {
				try {
					v = JSON.stringify(v);
				} catch (e) {
					v = e;
				}
			}
			try {
				fs.writeSync(logger, (new Date()).toISOString() + " " + sanitizeLog(v.toString()) + os.EOL);
			} catch (e) {
				console_error(e);
			}
		}
	});
}
function open_log(callback) {
	if (!logger) {
		try {
			if (ensureFileDirectorySync(log_file, callback, false) !== null) return;
			logger = fs.openSync(log_file, "a");
		} catch (e) {
			callback(e);
			return;
		}
	}
	callback(null);
}
function printSize(bytes) {
	var units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
	for (var steps = 0; bytes > 1024; steps++) {
		bytes /= 1024;
	}
	return parseFloat(bytes).toFixed(2) + " " + units[steps];
}
function obj_filter(obj_in, obj_match) {
	var obj_out = {};
	for (const [key, value] of Object.entries(obj_in)) {
		if (obj_match.hasOwnProperty(key)) {
			if (obj_match[key] === true || typeof value !== "object") {
				obj_out[key] = value;
			} else {
				obj_out[key] = obj_filter(value, obj_match[key]);
			}
		}
	}
	return obj_out;
}
function file_path_absolute(file, dir=directory) {
	return (path.isAbsolute(file) ? "" : dir + path.sep) + file;
}
process.on("exit", function() {
	if (log_save && logger) {
		try {
			fs.closeSync(logger);
		} catch (e) {}
	}
});
process.on("SIGINT", function() {
	running = false;
	console_log("SIGINT");
	if (timeout !== null) {
		clearTimeout(timeout);
		timeout = null;
	}
});
(function() {
	var i, t, a = parseArgv(process.argv), f;
	if (a.e !== undefined) {
		f = function(error, data) {
			if (error !== null) {
				console_error(error);
				return;
			}
			commands_file_cmds = data;
			commands_file_uids = a.i;
			if (a.text !== undefined) {
				commands_file_func = getUidsLiteral;
			} else if (commands_file_uids !== undefined && commands_file_uids.length > 0 && path.extname(commands_file_uids) === ".json") {
				commands_file_func = a.ids !== undefined ? getIdsJson : getUidsJson;
			} else {
				commands_file_func = a.ids !== undefined ? getIdsSync : getUidsSync;
			}
			var cmds = getCmds(commands_file_cmds);
//			console_log(cmds);
//			return;
			if (a.d !== undefined) {
				try {
					ids_save_to_file(cmds, root_directory + path.sep + output_file_name(a, "cmds", false));
				} catch (e) {
					console_error(e);
					return;
				}
			}
			execCmds(cmds,
			a.r !== undefined && !isNaN(i = parseInt(a.r, 10)) && i > -1 ? i * 1000 : -1,
			a.p !== undefined && !isNaN(i = parseInt(a.p, 10)) && i > 1 ? i : 1,
			a.o !== undefined,
			a.s !== undefined,
			a.u !== undefined, emptyFunction);
		};
		if (a.e.length === 0) {
			readStdIn(f);
		} else if (a.f !== undefined) {
			f(null, a.e);
		} else {
			commands_file = file_path_absolute(a.e, root_directory);
			fs.readFile(commands_file, "utf8", f);
		}
		return;
	} else if (a.t === undefined) {
		console_log(help);
		return;
	} else if (a.f !== undefined) {
		directory = file_path_absolute(a.f, root_directory);
	}
	t = ({
		"ui": 0,
		"mi": 1,
		"hi": 2,
		"vi": 3,
		"uv": 4,
		"mv": 5,
		"hv": 6,
		"v": 7,
		"us": 8,
		"s": 9,
		"c": 10,
		"cv": 11,
		"ul": 12,
		"fyp": 13,
		"vc": 14,
		"cm": 15,
		"vcr": 16,
		"followed": 17,
		"uvw": 18,
		"uvwp": 19
	})[a.t.toLowerCase()];
	if (a.si !== undefined && [4,12].includes(t)) {
		a.i = "";
	}
	if (a.si === undefined && [18].includes(t)) {
		console_log(help);
		return;
	}
	if (a.i === undefined && ![9,10,13,15,17,18].includes(t)) {
		console_log(help);
		return;
	}
	if (a.j !== undefined) {
		cookie = a.j;
	}
	if (a.cookie !== undefined) {
		cookie = ["sid_tt", "sessionid", "sessionid_ss"].map(v => v + "=" + a.cookie).join("; ");
	}
	if (a.u !== undefined) {
		user_agent = a.u;
	}
	if (a.o !== undefined && !isNaN(i = parseInt(a.o, 10))) {
		cursor = i;
	}
	if (user_agents_random || a.ur !== undefined) {
//		try {
//			user_agents = require("user-agents"),
//			user_agents = JSON.parse(fs.readFileSync((__dirname + path.sep + "user_agents.json"), {flag: "r", encoding: "utf8"})),
			user_agents_random = true;
//		} catch (e) {
//			console_error(e);
//			user_agents_random = false;
//		}
	}
	if (a.l !== undefined) {
		if ([4,18].includes(t) && a.l === "last_scrape") {
			last_scrape_save = true;
		} else if ([4,18].includes(t) && a.l.length === 10 && !isNaN(i = Math.floor((new Date(a.l + "T00:00:00.000Z")).getTime() / 1000))) {
			limit = i,
			date_limit = true;
		} else if (a.l.length > 0 && !isNaN(i = parseInt(a.l, 10))) {
			limit = i,
			date_limit = false;
		}
	}
	if (a.q !== undefined) {
		tag_wm = true,
		tag_sd = false,
		download_wm = false,
		download_sd = true,
		download_hd = false,
		fallback_sd = false, // defaults
		quality = true, // deprecated for videos, still used in some other places
		tag_sd = true;
		if ((a.q.length > 0) && (!isNaN(i = parseInt(a.q, 10)))) {
			if (i === -1) {
				tag_wm = false,
				download_wm = true,
				download_sd = false;
			} else if (i === 0) {
				quality = false;
			} else if (i === 1) {
				download_hd = true;
			} else if (i === 2 || i === -2) {
				verify_files = true,
				download_sd = false,
				download_hd = true,
				fallback_sd = true,
				tag_sd = i === 2;
			} else if (i === 3) {
				download_wm = true,
				download_sd = false;
			} else if (i === 4) {
				download_wm = true,
				download_hd = true;
			}
		} else {
			download_sd = false,
			download_hd = true;
		}
	}
	if ((a.d !== undefined) && (a.d.length > 0) && (!isNaN(i = parseInt(a.d, 10)))) {
		description_limit = Math.min(i, 255);
	}
	if ((a.w !== undefined) && (a.w.length > 0) && (!isNaN(i = parseInt(a.w, 10)))) {
		count = Math.min(Math.max(i, 1), 100);
	}
	if ((a.n !== undefined) && (a.n.length > 0) && (!isNaN(i = parseInt(a.n, 10)))) {
		ignore_errors_limit = Math.max(i, 0);
	}
	if ((a.error_delay !== undefined) && (a.error_delay.length > 0) && (!isNaN(i = parseInt(a.error_delay, 10)))) {
		ignore_errors_delay = Math.max(i, 0);
	}
	if ((a.aid !== undefined) && (a.aid.length > 0) && (!isNaN(i = parseInt(a.aid, 10)))) {
		aid = Math.max(i, 0);
//		aid = Math.floor(Math.random() * 1000);
	}
	if ((a.max_depth !== undefined) && (a.max_depth.length > 0) && (!isNaN(i = parseInt(a.max_depth, 10)))) {
		max_depth = Math.max(i, 0);
	}
	if ((a.max_resumes !== undefined) && (a.max_resumes.length > 0) && (!isNaN(i = parseInt(a.max_resumes, 10)))) {
		max_resumes = Math.min(Math.max(i, 1), 100);
	}
	if ((a.max_retries !== undefined) && (a.max_retries.length > 0) && (!isNaN(i = parseInt(a.max_retries, 10)))) {
		max_retries = Math.min(Math.max(i, 1), 100);
	}
	if ((a.max_converts !== undefined) && (a.max_converts.length > 0) && (!isNaN(i = parseInt(a.max_converts, 10)))) {
		max_converts = Math.min(Math.max(i, 1), 100);
	}
	if ((a.max_downloads !== undefined) && (a.max_downloads.length > 0) && (!isNaN(i = parseInt(a.max_downloads, 10)))) {
		max_downloads = Math.min(Math.max(i, 1), 100);
	}
	if ((a.max_zips !== undefined) && (a.max_zips.length > 0) && (!isNaN(i = parseInt(a.max_zips, 10)))) {
		max_zips = Math.min(Math.max(i, 1), 100);
	}
	if ((a.min_views !== undefined) && (a.min_views.length > 0) && (!isNaN(i = parseInt(a.min_views, 10)))) {
		skip_min_views = Math.max(i, 0);
	}
	if ((a.max_views !== undefined) && (a.max_views.length > 0) && (!isNaN(i = parseInt(a.max_views, 10)))) {
		skip_max_views = Math.max(i, 0);
	}
	if (a.aweme_host_posts !== undefined && a.aweme_host_posts.length > 0) {
		aweme_host_posts = a.aweme_host_posts;
	}
	if (a.aweme_host_details !== undefined && a.aweme_host_details.length > 0) {
		aweme_host_details = a.aweme_host_details;
	}
	if (a.aweme_host_likes !== undefined && a.aweme_host_likes.length > 0) {
		aweme_host_likes = a.aweme_host_likes;
	}
	if (a.aweme_host_music !== undefined && a.aweme_host_music.length > 0) {
		aweme_host_music = a.aweme_host_music;
	}
	if (a.aweme_host_tags !== undefined && a.aweme_host_tags.length > 0) {
		aweme_host_tags = a.aweme_host_tags;
	}
	if (a.aweme_host_stories !== undefined && a.aweme_host_stories.length > 0) {
		aweme_host_stories = a.aweme_host_stories;
	}
	if (a.aweme_host_comments !== undefined && a.aweme_host_comments.length > 0) {
		aweme_host_comments = a.aweme_host_comments;
	}
	if (a.aweme_host_collections !== undefined && a.aweme_host_collections.length > 0) {
		aweme_host_collections = a.aweme_host_collections;
	}
	if (a.aweme_host_trending !== undefined && a.aweme_host_trending.length > 0) {
		aweme_host_trending = a.aweme_host_trending;
	}
	if (a.aweme_host_followed !== undefined && a.aweme_host_followed.length > 0) {
		aweme_host_followed = a.aweme_host_followed;
	}
	if (a.aweme_host !== undefined && a.aweme_host.length > 0) {
		var api_pos = aweme_hosts.indexOf(a.aweme_host);
		if (api_pos > 0) aweme_hosts.splice(api_pos, 1);
		if (api_pos !== 0) aweme_hosts.unshift(a.aweme_host);
	}
	date = date || a.c !== undefined,
	description = description || a.d !== undefined,
	hevc = hevc || a.h !== undefined,
	last_scrape_save = [4,18].includes(t) && (last_scrape_save || a.last_scrape !== undefined),
	log_save = log_save || a.g !== undefined,
	log_errors = log_errors || a.log_errors !== undefined,
	log_verbose = log_verbose || a.log_verbose !== undefined,
	cursor_print = cursor_print || a.x !== undefined,
	cursor_times = cursor_times || a.x !== undefined && a.x.length > 0,
	break_limit = break_limit || a.y !== undefined,
	ignore_errors = ignore_errors || a.n !== undefined,
	uid_directories = uid_directories || a.z !== undefined,
	cycle_hosts = cycle_hosts || a.cycle_hosts !== undefined,
	debug = debug || a.debug !== undefined,
	debug_data = debug_data || a.debug_data !== undefined,
	alert = alert || a.alert_proc !== undefined && a.alert_proc.length > 0,
	tag_uid = tag_uid || a.ui !== undefined,
	tag_unique_id = tag_unique_id || a.un !== undefined,
	tag_mutual = tag_mutual || a.tag_mutual !== undefined,
	tag_stories = tag_stories || a.tag_stories !== undefined,
	strip_spaces = strip_spaces || a.strip_spaces !== undefined,
	windows_paths = windows_paths || a.windows_paths !== undefined,
	progress_bars = (progress_bars || a.progress_bars !== undefined) && progress_bars_stream.isTTY,
	progress_bars_debug = progress_bars_debug || a.progress_bars !== undefined && a.progress_bars.length > 0,
	fetch_only = fetch_only || a.fetch_only !== undefined,
	download_videos = a.p === undefined && !fetch_only,
	download_videos_tikwm = download_videos_tikwm || a.tikwm !== undefined,
	download_videos_single = download_videos_single || a.aweme_details_single !== undefined,
	download_thumbs = (download_thumbs || a.a !== undefined) && !fetch_only,
	download_thumbs_anim = (download_thumbs_anim || a.b !== undefined) && !fetch_only,
	zip_photos = zip_photos || a.zip_photos !== undefined,
	download_files = download_videos || download_thumbs || download_thumbs_anim,
	metadata_verbose = metadata_verbose || a.v !== undefined,
	metadata_split = metadata_split || a.s !== undefined,
	metadata_print = metadata_print || a.k !== undefined,
	metadata_save = ([14,15,16].includes(t) || metadata_save || a.m !== undefined) && !fetch_only,
	metadata_save_ignored = metadata_save && (metadata_save_ignored || a.mi !== undefined) && !fetch_only,
	metadata_save_each = (metadata_save_each || a.metadata_each !== undefined) && !fetch_only,
	metadata_update_each = metadata_update_each || a.metadata_each !== undefined && a.metadata_each.length > 0,
	metadata_dirs = metadata_dirs || a.metadata_dirs !== undefined,
	metadata_dir_name = a.metadata_dirs !== undefined && a.metadata_dirs.length > 0 ? a.metadata_dirs : metadata_dir_name,
	story_dirs = story_dirs || a.story_dirs !== undefined,
	story_dir_name = a.story_dirs !== undefined && a.story_dirs.length > 0 ? a.story_dirs : story_dir_name,
	fetched_ids_save = fetched_ids_save || a.fetched_ids !== undefined,
	scraped_ids_save = (scraped_ids_save || a.scraped_ids !== undefined) && !fetch_only,
	unscraped_ids_save = (unscraped_ids_save || a.unscraped_ids !== undefined) && !fetch_only,
	ignore_ids_save = (ignore_ids_save || a.ignore_ids !== undefined) && !fetch_only,
	save_empty = save_empty || a.save_empty !== undefined,
	skip_photos = skip_photos || a.skip_photos !== undefined,
	skip_stories = ![8,9].includes(t) && (skip_stories || a.skip_stories !== undefined),
	skip_followed = skip_followed || a.skip_followed !== undefined,
	only_photos = only_photos || a.only_photos !== undefined,
	only_stories = only_stories || a.only_stories !== undefined,
	only_followed = only_followed || a.only_followed !== undefined,
	verify_files = verify_files || a.verify_files !== undefined,
	rename_hevc = rename_hevc || a.rename_hevc !== undefined,
	convert_hevc = convert_hevc || a.convert_hevc !== undefined,
	require_ffprobe = verify_files || rename_hevc || convert_hevc,
	request_aweme_cache = t === 4 && (request_aweme_cache || a.request_aweme_cache !== undefined) && a.skip_aweme_cache === undefined,
	request_aweme_static = request_aweme_static || a.static_uri !== undefined;
	if (progress_bars) {
		multi_progress = new (require("multi-progress"))(progress_bars_stream);
		(require("cli-cursor")).hide();
	}
	if (zip_photos) {
		archiver = require("archiver");
	}
	if (request_aweme_static) {
		var request_aweme_static_file = request_aweme_static_dir + path.sep + a.i + ".json";
		try {
			request_aweme_static = JSON.parse(fs.readFileSync(file_path_absolute(request_aweme_static_file, root_directory), {flag: "r", encoding: "utf8"}));
			max_depth = 1;
		} catch (e) {
			request_aweme_static = false;
			console_error(e);
			return;
		}
	}
	if (alert) {
		if (a.alert_proc !== undefined) alert_proc = a.alert_proc;
		if (a.alert_args !== undefined) alert_args = a.alert_args;
	}
	if (log_save && a.g !== undefined && a.g.length > 0) {
		log_file = a.g;
	}
	if (metadata_save) {
		metadata_file = a.m !== undefined && a.m.length > 0 ? a.m : output_file_name(a);
	}
	if (fetched_ids_save) {
		fetched_ids_file = a.fetched_ids !== undefined && a.fetched_ids.length > 0 ? a.fetched_ids : output_file_name(a, "fetched");
	}
	if (scraped_ids_save) {
		scraped_ids_file = a.scraped_ids !== undefined && a.scraped_ids.length > 0 ? a.scraped_ids : output_file_name(a, "scraped");
	}
	if (unscraped_ids_save) {
		unscraped_ids_file = a.unscraped_ids !== undefined && a.unscraped_ids.length > 0 ? a.unscraped_ids : output_file_name(a, "unscraped");
	}
	if (ignore_ids_save) {
		if (a.ignore_ids !== undefined && a.ignore_ids.length > 0) {
			ignore_ids_file = a.ignore_ids;
		} else if ([4,5,6,8,18].includes(t)) {
			ignore_ids_file = ignore_ids_dir + path.sep + {4:"@",5:"~",6:"#",8:"$",18:"@"}[t] + a.i + ".json";
		} else {
			ignore_ids_save = ignore_ids_file !== undefined && ignore_ids_file.length > 0;
		}
		if (ignore_ids_save) {
			try {
				ignore_ids = new Set(JSON.parse(fs.readFileSync(file_path_absolute(ignore_ids_file), {flag: "r", encoding: "utf8"})));
			} catch (e) {
				ignore_ids = new Set();
			}
		}
		ignore_ids_initial = new Set(ignore_ids);
	}
	if (skip_uids || a.skip_uids !== undefined) {
		if (a.skip_uids !== undefined && a.skip_uids.length > 0) skip_uids_file = a.skip_uids;
		try {
			skip_uids = new Set(getUidsSync(fs.readFileSync(file_path_absolute(skip_uids_file, root_directory), {flag: "r", encoding: "utf8"})));
		} catch (e) {
			skip_uids = false;
			console_error(e);
		}
	}
	if (only_uids || a.only_uids !== undefined) {
		if (a.only_uids !== undefined && a.only_uids.length > 0) only_uids_file = a.only_uids;
		try {
			only_uids = new Set(getUidsSync(fs.readFileSync(file_path_absolute(only_uids_file, root_directory), {flag: "r", encoding: "utf8"})));
		} catch (e) {
			only_uids = false;
			console_error(e);
		}
	}
	if (last_scrape_save) {
		last_scrape_file = a.last_scrape !== undefined && a.last_scrape.length > 0 ? a.last_scrape : last_scrapes_dir + path.sep + "@" + a.i + ".txt";
		if (a.l === "last_scrape") {
			try {
				if (isNaN(previous_scrape = +fs.readFileSync(file_path_absolute(last_scrape_file), {flag: "r", encoding: "utf8"}))) throw "Invalid file contents";
				limit = previous_scrape,
				date_limit = true;
			} catch (e) {
				previous_scrape = 0,
				limit = 1,
				date_limit = false,
				break_limit = true;
			}
		}
	}
	if (require_ffprobe) {
		ffprobe = require("ffprobe");
		if (ffprobe_path === undefined || (convert_hevc && ffmpeg_path === undefined)) {
			ffmpeg = require("ffmpeg-ffprobe-static");
		}
		if (convert_hevc) {
			ffmpeg_path = ffmpeg_path !== undefined ? ffmpeg_path : ffmpeg.ffmpegPath;
		}
		ffprobe_path = ffprobe_path !== undefined ? ffprobe_path : ffmpeg.ffprobePath;
	}
	switch (t) {
		case 0:
			if (a.i.length === 0) {
				break;
			} else if (a.i.charCodeAt(0) === 64) {
				if (a.i.length === 1) {
					console_error(new Error("Name is empty."));
					return;
				}
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			infoUser(a.i, emptyFunction);
			return;
		case 1:
			if (a.i.length === 0) {
				break;
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			infoMusic(a.i, emptyFunction);
			return;
		case 2:
			if (a.i.length === 0) {
				break;
			} else if (a.i.charCodeAt(0) === 35) {
				if (a.i.length === 1) {
					console_error(new Error("Tag is empty."));
					return;
				}
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			infoHashtag(a.i, emptyFunction);
			return;
		case 3:
			if (a.i.length === 0) {
				break;
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			infoVideo(a.i, emptyFunction);
			return;
		case 4:
			if (a.si !== undefined) {
				sec_uid = a.si;
			} else if (a.i.length === 0) {
				break;
			} else if (a.i.charCodeAt(0) === 64) {
				if (a.i.length === 1) {
					console_error(new Error("Name is empty."));
					return;
				}
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			downloadUserVideos(a.i, emptyFunction);
			return;
		case 5:
			if (a.i.length === 0) {
				break;
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			downloadMusicVideos(a.i, emptyFunction);
			return;
		case 6:
			if (a.i.length === 0) {
				break;
			} else if (a.i.charCodeAt(0) === 35) {
				if (a.i.length === 1) {
					console_error(new Error("Tag is empty."));
					return;
				}
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			downloadHashtagVideos(a.i, emptyFunction);
			return;
		case 7:
			var download_videos_func = download_videos_single ? downloadVideos : downloadVideosMulti;
			if (idRegex.test(a.i)) {
				download_videos_func([a.i], emptyFunction);
			} else {
				f = function(error, data) {
					if (error !== null) {
						console_error(error);
						return;
					}
					getIds(data, function(error, ids) {
						if (error !== null) {
							console_error(error);
						} else if (ids.length > 0) {
							download_videos_func(ids, emptyFunction);
						}
					});
				};
				if (a.i.length === 0) {
					readStdIn(f);
				} else if (a.i === "clipboard") {
					try {
						f(null, (require("clipboardy")).readSync());
					} catch (e) {
						console_error(e);
						return;
					}
				} else {
					fs.readFile(file_path_absolute(a.i, root_directory), "utf8", f);
				}
			}
			return;
		case 8:
			if (a.i.length === 0) {
				break;
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			downloadUserStories(a.i, emptyFunction);
			return;
		case 9:
			scrapeStories(emptyFunction);
			return;
		case 10:
			scrapeCollections(emptyFunction);
			return;
		case 11:
			if (a.i.length === 0) {
				break;
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			downloadCollection(a.i, emptyFunction);
			return;
		case 12:
			if (a.si !== undefined) {
				sec_uid = a.si;
			} else if (a.i.length === 0) {
				break;
			} else if (a.i.charCodeAt(0) === 64) {
				if (a.i.length === 1) {
					console_error(new Error("Name is empty."));
					return;
				}
			} else if (!idRegex.test(a.i)) {
				console_error(new Error("Id is not valid."));
				return;
			}
			downloadUserLikes(a.i, emptyFunction);
			return;
		case 13:
			downloadTrending(emptyFunction);
			return;
		case 14:
			var cid = a.i;
			if (cid.length === 0) break;
			if (!idRegex.test(cid)) {
				if (cid.indexOf("?cid=") > -1) {
					try {
						cid = (new URL(cid, "https://www.tiktok.com")).searchParams.get("cid");
					} catch (e) {}
				}
				try {
					cid = Buffer.from(cid, "base64").toString();
				} catch (e) {}
				if (!idRegex.test(cid)) {
					console_error(new Error("Id is not valid."));
					return;
				}
			}
			dumpVideoComments(cid, emptyFunction);
			return;
		case 15:
			dumpCollectionsMetadata(emptyFunction);
			return;
		case 16:
			var cid = a.i;
			if (cid.length === 0) break;
			if (!idRegex.test(cid)) {
				if (cid.indexOf("?cid=") > -1) {
					try {
						cid = (new URL(cid, "https://www.tiktok.com")).searchParams.get("cid");
					} catch (e) {}
				}
				try {
					cid = Buffer.from(cid, "base64").toString();
				} catch (e) {}
				if (!idRegex.test(cid)) {
					console_error(new Error("Id is not valid."));
					return;
				}
			}
			dumpVideoCommentReplies(cid, emptyFunction);
			return;
		case 17:
			downloadFollowedAweme(emptyFunction);
			return;
		case 18:
			count = Math.min(count, 35);
			downloadUserVideosWeb(a.si, emptyFunction);
			return;
		case 19:
			downloadUserVideosWebPage(a.i, emptyFunction);
			return;
	}
	console_log(help);
})();