Compare commits

..

No commits in common. "trunk" and "iss10" have entirely different histories.
trunk ... iss10

27 changed files with 301 additions and 301 deletions

View file

@ -1,2 +1,2 @@
install: install:
cp dist/pyinstaller/manylinux_2_39_x86_64/doomcc /home/tynan/.local/bin/dcc cp dist/pyinstaller/manylinux_2_39_x86_64/dcc /home/tynan/.local/bin/dcc

5
dcc/__main__.py Normal file
View file

@ -0,0 +1,5 @@
import sys
from dcc.main import main
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View file

@ -1,6 +1,6 @@
import av import av
import copy import copy
import doomcc.doom_base import dcc.doom_base
import enum import enum
import fractions import fractions
import io import io
@ -16,7 +16,7 @@ class State(enum.Enum):
DONE = 3 DONE = 3
class Concat(doomcc.doom_base.Wad): class Concat(dcc.doom_base.Wad):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("start_map") parser.add_argument("start_map")
@ -34,10 +34,12 @@ class Concat(doomcc.doom_base.Wad):
+ f"to{parsed_args.end_map}" + f"to{parsed_args.end_map}"
) )
output = av.open( output = av.open(
self.fabricate.joinpath(parsed_args.wad).joinpath(f"{fn_base}.mp4"), "w" self.fabricate.joinpath(parsed_args.wad).joinpath(
f"{fn_base}.mp4"), "w"
) )
summary_file = open( summary_file = open(
self.fabricate.joinpath(parsed_args.wad).joinpath(f"{fn_base}.txt"), "w" self.fabricate.joinpath(parsed_args.wad).joinpath(
f"{fn_base}.txt"), "w"
) )
self._offset = 0 self._offset = 0
@ -64,13 +66,11 @@ class Concat(doomcc.doom_base.Wad):
start_time = self._offset / 1000000 start_time = self._offset / 1000000
text = self._add_chunk( text = self._add_chunk(
videodir.joinpath(f"{parsed_args.wad}_map{idx}.mp4"), videodir.joinpath(f"{parsed_args.wad}_map{idx}.mp4"),
output, output, not parsed_args.nooverlay
not parsed_args.nooverlay,
) )
list.append( list.append(
summary, summary, f"{text} {math.floor(start_time / 60):02}:"
f"{text} {math.floor(start_time / 60):02}:" + f"{math.floor(start_time % 60):02}"
+ f"{math.floor(start_time % 60):02}",
) )
if state == State.DONE: if state == State.DONE:
break break
@ -82,7 +82,8 @@ class Concat(doomcc.doom_base.Wad):
def _add_chunk(self, v, output, overlay): def _add_chunk(self, v, output, overlay):
chunk = av.open(v) chunk = av.open(v)
if not (len(chunk.streams.video) == 1 and len(chunk.streams.audio) == 1): if not (len(chunk.streams.video) == 1
and len(chunk.streams.audio) == 1):
raise Exception( raise Exception(
f"irregular chunk {v}: streams {chunk.streams} " f"irregular chunk {v}: streams {chunk.streams} "
+ f"(expected 1 video & 1 audio)" + f"(expected 1 video & 1 audio)"
@ -95,11 +96,16 @@ class Concat(doomcc.doom_base.Wad):
text = "" text = ""
if overlay: if overlay:
img = wand.image.Image( img = wand.image.Image(
height=chunk.streams[0].height, width=chunk.streams[0].width height=chunk.streams[0].height,
width=chunk.streams[0].width
) )
mapstring = v.name[-6:-4] mapstring = v.name[-6:-4]
text = self._config["map_names"][f"map{mapstring}"] text = self._config["map_names"][f"map{mapstring}"]
self.draw_text(img, f"MAP{mapstring}: {text}", font_size=120) self.draw_text(
img,
f"MAP{mapstring}: {text}",
font_size=120
)
img.trim(reset_coords=True) img.trim(reset_coords=True)
img.border("graya(25%, 25%)", 10, 10) img.border("graya(25%, 25%)", 10, 10)
img.border(self.thumbnail_text_stroke, 16, 16) img.border(self.thumbnail_text_stroke, 16, 16)
@ -107,14 +113,17 @@ class Concat(doomcc.doom_base.Wad):
# multiple of 8. dude whyyyyyyy # multiple of 8. dude whyyyyyyy
padfactor = 8 padfactor = 8
img.border("transparent", padfactor, 0) img.border("transparent", padfactor, 0)
img.crop(width=img.width - img.width % padfactor, height=img.height) img.crop(
width=img.width - img.width % padfactor,
height=img.height
)
if len(output.streams.get()) == 0: if len(output.streams.get()) == 0:
# We can't use the input stream as a template here; it doesn't # We can't use the input stream as a template here; it doesn't
# have everything needed to do encoding and will fail # have everything needed to do encoding and will fail
# mysteriously later. # mysteriously later.
vs = chunk.streams.video[0] vs = chunk.streams.video[0]
vr = int(vs.time_base.denominator / vs.time_base.numerator) vr = int(vs.time_base.denominator/vs.time_base.numerator)
ovs = output.add_stream("h264", rate=vr) ovs = output.add_stream("h264", rate=vr)
ovs.extradata = copy.deepcopy(vs.extradata) ovs.extradata = copy.deepcopy(vs.extradata)
ovs.height = vs.height ovs.height = vs.height
@ -136,10 +145,12 @@ class Concat(doomcc.doom_base.Wad):
oas.bit_rate = astr.bit_rate oas.bit_rate = astr.bit_rate
src = ograph.add_buffer( src = ograph.add_buffer(
template=chunk.streams.video[0], time_base=chunk.streams.video[0].time_base template=chunk.streams.video[0],
time_base=chunk.streams.video[0].time_base
) )
asrc = ograph.add_abuffer( asrc = ograph.add_abuffer(
template=chunk.streams.audio[0], time_base=chunk.streams.audio[0].time_base template=chunk.streams.audio[0],
time_base=chunk.streams.audio[0].time_base
) )
# TODO: video fades are absolute relative to the input video; audio # TODO: video fades are absolute relative to the input video; audio
# fades need to have their timestamps offset by the position in the # fades need to have their timestamps offset by the position in the
@ -152,18 +163,19 @@ class Concat(doomcc.doom_base.Wad):
iafade_start = self._offset * sample_rate / 1000000 iafade_start = self._offset * sample_rate / 1000000
iafade = ograph.add("afade", args=f"in:{iafade_start}:{sample_rate}") iafade = ograph.add("afade", args=f"in:{iafade_start}:{sample_rate}")
oafade_start = ( oafade_start = (
self._offset + chunk.duration (self._offset + chunk.duration) * sample_rate / 1000000
) * sample_rate / 1000000 - sample_rate - sample_rate
)
oafade = ograph.add("afade", args=f"out:{oafade_start}:{sample_rate}") oafade = ograph.add("afade", args=f"out:{oafade_start}:{sample_rate}")
if overlay: if overlay:
overlay = ograph.add_buffer( overlay = ograph.add_buffer(
width=img.width, width=img.width, height=img.height,
height=img.height, format="rgba", time_base=chunk.streams.video[0].time_base
format="rgba", )
time_base=chunk.streams.video[0].time_base, overlay_fo = ograph.add(
"fade", args=f"out:{4 * frame_rate}:{frame_rate}"
) )
overlay_fo = ograph.add("fade", args=f"out:{4 * frame_rate}:{frame_rate}")
overlay.link_to(overlay_fo, 0, 0) overlay.link_to(overlay_fo, 0, 0)
composite = ograph.add("overlay", args="x=4:y=4") composite = ograph.add("overlay", args="x=4:y=4")
src.link_to(composite, 0, 0) src.link_to(composite, 0, 0)
@ -182,8 +194,9 @@ class Concat(doomcc.doom_base.Wad):
for packet in chunk.demux(): for packet in chunk.demux():
if packet.dts is None: if packet.dts is None:
continue continue
pof = (self._offset * packet.time_base.denominator) / ( pof = (
packet.time_base.numerator * 1000000 (self._offset * packet.time_base.denominator)
/ (packet.time_base.numerator * 1000000)
) )
packet.dts += pof packet.dts += pof
packet.pts += pof packet.pts += pof
@ -208,7 +221,9 @@ class Concat(doomcc.doom_base.Wad):
def _make_text_frame(self, img, ifr): def _make_text_frame(self, img, ifr):
# We need to give each frame its own memory it can own. # We need to give each frame its own memory it can own.
text_frame = av.video.frame.VideoFrame(img.width, img.height, format="rgba") text_frame = av.video.frame.VideoFrame(
img.width, img.height, format="rgba"
)
text_frame.planes[0].update(img.make_blob(format="rgba")) text_frame.planes[0].update(img.make_blob(format="rgba"))
text_frame.pts = ifr.pts text_frame.pts = ifr.pts
text_frame.dts = ifr.dts text_frame.dts = ifr.dts

View file

@ -12,15 +12,13 @@ class ConfigBase(object):
self._doom = pathlib.Path(parsed_args.doom) self._doom = pathlib.Path(parsed_args.doom)
self._config_name = parsed_args.config_name self._config_name = parsed_args.config_name
self._config = tomlkit.toml_file.TOMLFile( self._config = tomlkit.toml_file.TOMLFile(
self.doom.joinpath(self.config_name) self.doom.joinpath(self.config_name)).read()
).read()
self._dsda = self._config.get("dsda") self._dsda = self._config.get("dsda")
if self._dsda is None: if self._dsda is None:
raise Exception( raise Exception(
"required key 'dsda' not set in config " "required key 'dsda' not set in config "
+ f"{self.doom.joinpath(self.config_name)}." + f"{self.doom.joinpath(self.config_name)}.")
)
for d in ("iwads", "pwads", "demos", "fabricate"): for d in ("iwads", "pwads", "demos", "fabricate"):
self._init_attr([d], d, fn=self.doom.joinpath) self._init_attr([d], d, fn=self.doom.joinpath)
@ -31,7 +29,10 @@ class ConfigBase(object):
self._init_attr(["thumbnail", "text_fill"], "white") self._init_attr(["thumbnail", "text_fill"], "white")
self._init_attr(["thumbnail", "text_stroke"], "red") self._init_attr(["thumbnail", "text_stroke"], "red")
self._init_attr(["thumbnail", "overlay_name"], "M_DOOM_scaled.png") self._init_attr(["thumbnail", "overlay_name"], "M_DOOM_scaled.png")
self._init_attr(["fetch", "mirror"], "https://youfailit.net/pub/idgames") self._init_attr(
["fetch", "mirror"],
"https://youfailit.net/pub/idgames"
)
def _init_attr(self, what, default, fn=lambda x: x): def _init_attr(self, what, default, fn=lambda x: x):
propname = "_".join(what) propname = "_".join(what)
@ -44,7 +45,8 @@ class ConfigBase(object):
setattr(self, f"_{propname}", fn(val)) setattr(self, f"_{propname}", fn(val))
setattr( setattr(
type(self), propname, property(lambda self: getattr(self, f"_{propname}")) type(self), propname,
property(lambda self: getattr(self, f"_{propname}"))
) )
@property @property
@ -63,10 +65,10 @@ class ConfigBase(object):
def get_parser_func(toc): def get_parser_func(toc):
def add_common_args(self, prog_name): def add_common_args(self, prog_name):
parser = super(toc, self).get_parser(prog_name) parser = super(toc, self).get_parser(prog_name)
parser.add_argument("--doom", default=pathlib.Path.home().joinpath("doom")) parser.add_argument(
"--doom", default=pathlib.Path.home().joinpath("doom"))
parser.add_argument("--config-name", default="config.toml") parser.add_argument("--config-name", default="config.toml")
return parser return parser
return add_common_args return add_common_args

View file

@ -1,9 +1,9 @@
import doomcc.doom_base import dcc.doom_base
import omg import omg
import tomlkit.toml_file import tomlkit.toml_file
class Configure(doomcc.doom_base.Wad): class Configure(dcc.doom_base.Wad):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("--complevel", "--cl") parser.add_argument("--complevel", "--cl")
@ -22,10 +22,12 @@ class Configure(doomcc.doom_base.Wad):
wad = omg.WadIO(w) wad = omg.WadIO(w)
complevel = wad.read("COMPLVL").decode("ascii").strip() complevel = wad.read("COMPLVL").decode("ascii").strip()
except Exception as e: except Exception as e:
print(f"Wad {w} likely has no lump COMPLVL (exception {e})") print(
f"Wad {w} likely has no lump COMPLVL (exception {e})")
if complevel is None: if complevel is None:
complevel = input("Complevel? ") complevel = input("Complevel? ")
doc = tomlkit.document() doc = tomlkit.document()
doc.add("complevel", complevel) doc.add("complevel", complevel)
if parsed_args.iwad is not None: if parsed_args.iwad is not None:

View file

@ -1,13 +1,13 @@
from abc import abstractmethod from abc import abstractmethod
from cliff.command import Command from cliff.command import Command
import doomcc.config import dcc.config
import io import io
import os import os
import re import re
import tomlkit import tomlkit
class Wad(doomcc.config.Base): class Wad(dcc.config.Base):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("wad") parser.add_argument("wad")
@ -70,8 +70,9 @@ class Wad(doomcc.config.Base):
return [] return []
def thumb_overlay_path(self): def thumb_overlay_path(self):
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath( return (
self.thumbnail_overlay_name self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self.thumbnail_overlay_name)
) )
@ -128,51 +129,55 @@ class WadMap(Wad):
def demo_in_path(self): def demo_in_path(self):
candidates = [ candidates = [
x for x in self.demos.joinpath(self.wad).glob(self._file_base("*.lmp")) x for x in self.demos.joinpath(self.wad)
.glob(self._file_base("*.lmp"))
] ]
if len(candidates) == 0: if len(candidates) == 0:
raise Exception( raise Exception(
"no suitable demo candidates for WAD {} MAP {}{}.".format( "no suitable demo candidates for WAD {} MAP {} name {}."
self.wad, .format(self.wad, self.map, self.name_string)
self.map,
f" name {self._name}" if self._name else "",
)
) )
if len(candidates) == 1: if len(candidates) == 1:
return candidates[0] return candidates[0]
return sorted(filter(lambda s: re.search("-", str(s)), candidates))[-1] return sorted(filter(lambda s: re.search("-", str(s)), candidates))[-1]
def dsda_text_path(self): def dsda_text_path(self):
return self._ensure(self.demos.joinpath(self.wad)).joinpath( return (
self._file_base(".txt") self._ensure(self.demos.joinpath(self.wad))
.joinpath(self._file_base(".txt"))
) )
def video_path(self): def video_path(self):
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath( return (
self._file_base(".mp4") self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base(".mp4"))
) )
def demo_out_path(self): def demo_out_path(self):
return self._ensure(self.demos.joinpath(self.wad)).joinpath( return (
self._file_base(".lmp") self._ensure(self.demos.joinpath(self.wad))
.joinpath(self._file_base(".lmp"))
) )
def target_bucket(self): def target_bucket(self):
return "doom/" + self._file_base(".lmp") return "doom/" + self._file_base(".lmp")
def base_thumb_path(self): def base_thumb_path(self):
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath( return (
self._file_base("_base.png") self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base("_base.png"))
) )
def text_thumb_path(self): def text_thumb_path(self):
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath( return (
self._file_base("_text.png") self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base("_text.png"))
) )
def thumb_path(self): def thumb_path(self):
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath( return (
self._file_base("_thumb.png") self._ensure(self.fabricate.joinpath(self.wad))
.joinpath(self._file_base("_thumb.png"))
) )
def _file_base(self, ext): def _file_base(self, ext):

View file

@ -1,5 +1,5 @@
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import os import os
import re import re
import shutil import shutil
@ -7,7 +7,7 @@ import subprocess
import zipfile import zipfile
class DSDA(doomcc.doom_base.WadMap): class DSDA(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("-s", "--single", action="store_true") parser.add_argument("-s", "--single", action="store_true")
@ -22,9 +22,10 @@ class DSDA(doomcc.doom_base.WadMap):
if shutil.which("xvfb-run") is not None: if shutil.which("xvfb-run") is not None:
command = ["xvfb-run"] + command command = ["xvfb-run"] + command
subprocess.run( subprocess.run(
command command + self.dsda_preamble(warp=False) + [
+ self.dsda_preamble(warp=False) "-fastdemo", dip, "-nosound",
+ ["-fastdemo", dip, "-nosound", "-skiptic", "-1", "-export_text_file"] "-skiptic", "-1", "-export_text_file"
]
) )
editor = "nano" editor = "nano"
if "EDITOR" in os.environ: if "EDITOR" in os.environ:
@ -37,7 +38,7 @@ class DSDA(doomcc.doom_base.WadMap):
else: else:
fh1 = self.wad[0:2] + self.map fh1 = self.wad[0:2] + self.map
if parsed_args.single: if parsed_args.single:
fh1 = self.wad[0 : min(len(self.wad), 4)] fh1 = self.wad[0:min(len(self.wad), 4)]
fh2 = "" fh2 = ""
with open(dtp, mode="r") as f: with open(dtp, mode="r") as f:
for line in f: for line in f:
@ -45,7 +46,7 @@ class DSDA(doomcc.doom_base.WadMap):
m = re.search("[^0-9]*([0-9]*):([0-9]*).[0-9]*", line) m = re.search("[^0-9]*([0-9]*):([0-9]*).[0-9]*", line)
if m is None: if m is None:
continue continue
fh2 = m[1] + m[2] fh2 = m[1]+m[2]
if len(fh2) % 2 == 1: if len(fh2) % 2 == 1:
fh2 = "0" + fh2 fh2 = "0" + fh2
break break

View file

@ -1,9 +1,9 @@
import doomcc.doom_base import dcc.doom_base
import doomcc.config import dcc.config
import os import os
class Eureka(doomcc.doom_base.WadMap): class Eureka(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("--main") parser.add_argument("--main")
@ -24,11 +24,7 @@ class Eureka(doomcc.doom_base.WadMap):
if complevel == "11" or complevel == "21": if complevel == "11" or complevel == "21":
port = "mbf" port = "mbf"
os.execvp( os.execvp("eureka",
"eureka", ["eureka"] + ["-iwad", iwad] + ["-w", parsed_args.map]
["eureka"] + ["-p", port] + [mw]
+ ["-iwad", iwad]
+ ["-w", parsed_args.map]
+ ["-p", port]
+ [mw],
) )

View file

@ -1,14 +1,14 @@
import doomcc.doom_base import dcc.doom_base
import omg import omg
import numpy as np import numpy as np
import wand.color import wand.color
import wand.image import wand.image
class Extract(doomcc.doom_base.Wad): class Extract(dcc.doom_base.Wad):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("lump") parser.add_argument('lump')
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
@ -24,9 +24,8 @@ class Extract(doomcc.doom_base.Wad):
) as img: ) as img:
img.transparent_color(wand.color.Color("#ff00ff"), 0.0) img.transparent_color(wand.color.Color("#ff00ff"), 0.0)
img.save( img.save(
filename=self.fabricate.joinpath(parsed_args.wad).joinpath( filename=self.fabricate.joinpath(parsed_args.wad)
parsed_args.lump + ".png" .joinpath(parsed_args.lump + ".png")
)
) )
return return
except Exception as e: except Exception as e:
@ -36,5 +35,6 @@ class Extract(doomcc.doom_base.Wad):
) )
print( print(
f"Lump {parsed_args.lump} not found in any wad in " + f"{parsed_args.wad}" f"Lump {parsed_args.lump} not found in any wad in "
+ f"{parsed_args.wad}"
) )

View file

@ -1,12 +1,12 @@
import contextlib import contextlib
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import os import os
import shutil import shutil
import tempfile import tempfile
class Fabricate(doomcc.doom_base.WadMap): class Fabricate(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("--fg", action="store_true") parser.add_argument("--fg", action="store_true")
@ -18,12 +18,10 @@ class Fabricate(doomcc.doom_base.WadMap):
command = [self.dsda] command = [self.dsda]
if not parsed_args.fg and shutil.which("xvfb-run") is not None: if not parsed_args.fg and shutil.which("xvfb-run") is not None:
command = ["xvfb-run"] + command command = ["xvfb-run"] + command
os.execvp( os.execvp(command[0],
command[0], command + self.dsda_preamble()
command
+ self.dsda_preamble()
+ ["-timedemo", self.demo_in_path()] + ["-timedemo", self.demo_in_path()]
+ ["-viddump", self.video_path()], + ["-viddump", self.video_path()]
) )
def options_dict(self): def options_dict(self):

View file

@ -1,4 +1,4 @@
import doomcc.config import dcc.config
import io import io
import json import json
import pathlib import pathlib
@ -9,7 +9,7 @@ import urllib.request
import zipfile import zipfile
class Fetch(doomcc.config.Base): class Fetch(dcc.config.Base):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("id_or_name") parser.add_argument("id_or_name")
@ -21,12 +21,14 @@ class Fetch(doomcc.config.Base):
idgames_id = self.search_idgames(parsed_args.id_or_name) idgames_id = self.search_idgames(parsed_args.id_or_name)
reply = self.fetch_url( reply = self.fetch_url(
"https://www.doomworld.com/idgames/api/" "https://www.doomworld.com/idgames/api/" +
+ "api.php?action=get&id={}&out=json".format(idgames_id) "api.php?action=get&id={}&out=json".format(idgames_id)
)
rpath = "/".join(
[self.fetch_mirror, reply["content"]["dir"], reply["content"]["filename"]]
) )
rpath = "/".join([
self.fetch_mirror,
reply["content"]["dir"],
reply["content"]["filename"]
])
wad = reply["content"]["filename"][0:-4] wad = reply["content"]["filename"][0:-4]
with urllib.request.urlopen(rpath) as response: with urllib.request.urlopen(rpath) as response:
@ -36,8 +38,8 @@ class Fetch(doomcc.config.Base):
# TODO: explicit error handling. Let users choose when >1 result. # TODO: explicit error handling. Let users choose when >1 result.
def search_idgames(self, wad): def search_idgames(self, wad):
reply = self.fetch_url( reply = self.fetch_url(
"https://www.doomworld.com/idgames/api/" "https://www.doomworld.com/idgames/api/" +
+ "api.php?action=search&query={}&out=json".format(wad) "api.php?action=search&query={}&out=json".format(wad)
) )
if "content" not in reply: if "content" not in reply:
sys.exit(f"No WAD named {wad} found on idgames.") sys.exit(f"No WAD named {wad} found on idgames.")
@ -60,5 +62,8 @@ class Fetch(doomcc.config.Base):
if fetcher_path is None: if fetcher_path is None:
raise Exception(f"Fetch util {fetcher} not found on PATH.") raise Exception(f"Fetch util {fetcher} not found on PATH.")
proc = subprocess.run([fetcher_path, url], capture_output=True, check=True) proc = subprocess.run(
[fetcher_path, url],
capture_output=True, check=True
)
return json.loads(proc.stdout) return json.loads(proc.stdout)

View file

@ -1,8 +1,8 @@
import doomcc.config import dcc.config
import os import os
class List(doomcc.config.ListerBase): class List(dcc.config.ListerBase):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("target") parser.add_argument("target")
@ -12,19 +12,22 @@ class List(doomcc.config.ListerBase):
match parsed_args.target: match parsed_args.target:
case "pwads": case "pwads":
return ( return (
("pwads",), ("pwads",), sorted(
sorted((x.name,) for x in os.scandir(self.pwads) if x.is_dir()), (x.name,) for x in os.scandir(self.pwads) if x.is_dir()
)
) )
case "iwads": case "iwads":
return ( return (
("iwads",), ("iwads",), sorted(
sorted((x.name,) for x in os.scandir(self.iwads) if x.is_file()), (x.name,) for x in
os.scandir(self.iwads) if x.is_file()
)
) )
case _: case _:
raise Exception(f"unknown target {parsed_args.target}") raise Exception(f"unknown target {parsed_args.target}")
class WadList(doomcc.config.ListerBase): class WadList(dcc.config.ListerBase):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("wad") parser.add_argument("wad")
@ -32,12 +35,11 @@ class WadList(doomcc.config.ListerBase):
def _get_results(self, name, base, wad, ext): def _get_results(self, name, base, wad, ext):
return ( return (
(name,), (name,), sorted(
sorted( (x.name,) for x in
(x.name,) os.scandir(base.joinpath(wad))
for x in os.scandir(base.joinpath(wad))
if x.name.endswith(ext) if x.name.endswith(ext)
), )
) )
@ -48,4 +50,6 @@ class ListDemos(WadList):
class ListVideos(WadList): class ListVideos(WadList):
def take_action(self, parsed_args): def take_action(self, parsed_args):
return self._get_results("videos", self.fabricate, parsed_args.wad, ".mp4") return self._get_results(
"videos", self.fabricate, parsed_args.wad, ".mp4"
)

72
dcc/main.py Normal file
View file

@ -0,0 +1,72 @@
import sys
from cliff.app import App
from cliff.commandmanager import CommandManager
import dcc.concat
import dcc.configure
import dcc.dsda
import dcc.eureka
import dcc.extract
import dcc.fabricate
import dcc.fetch
import dcc.ls
import dcc.pb
import dcc.play
import dcc.put
import dcc.record
import dcc.rib
import dcc.ss
import dcc.text
import dcc.thumb
class DCC(App):
def __init__(self):
cm = CommandManager(None)
commands = {
"concat": dcc.concat.Concat,
"configure": dcc.configure.Configure,
"dsda": dcc.dsda.DSDA,
"eureka": dcc.eureka.Eureka,
"extract": dcc.extract.Extract,
"fabricate": dcc.fabricate.Fabricate,
"fetch": dcc.fetch.Fetch,
"ls": dcc.ls.List,
"ls demos": dcc.ls.ListDemos,
"ls videos": dcc.ls.ListVideos,
"pb": dcc.pb.PB,
"play": dcc.play.Play,
"put": dcc.put.Put,
"record": dcc.record.Record,
"rib": dcc.rib.RIB,
"ss": dcc.ss.SS,
"text": dcc.text.Text,
"thumb": dcc.thumb.Thumb,
}
for n, c in commands.items():
cm.add_command(n, c)
super().__init__(
description="Doom Command Center",
version="0.0.1",
command_manager=cm,
deferred_help=True,
)
def initialize_app(self, argv):
pass
def prepare_to_run_command(self, cmd):
pass
def clean_up(self, cmd, result, err):
pass
def main(argv=sys.argv[1:]):
dcc = DCC()
return dcc.run(argv)
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,8 +1,8 @@
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import os import os
class PB(doomcc.doom_base.WadMap): class PB(dcc.doom_base.WadMap):
def take_action(self, parsed_args): def take_action(self, parsed_args):
os.execvp("ffplay", ["ffplay", self.video_path()]) os.execvp("ffplay", ["ffplay", self.video_path()])

View file

@ -1,8 +1,8 @@
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import os import os
class Play(doomcc.doom_base.WadMap): class Play(dcc.doom_base.WadMap):
def take_action(self, parsed_args): def take_action(self, parsed_args):
os.execv(self.dsda, [self.dsda] + self.dsda_preamble()) os.execv(self.dsda, [self.dsda] + self.dsda_preamble())

View file

@ -1,22 +1,23 @@
import boto3 import boto3
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
class Put(doomcc.doom_base.WadMap): class Put(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
return parser return parser
# TODO: accept configuration for bucket name # TODO: accept configuration for bucket name
def take_action(self, parsed_args): def take_action(self, parsed_args):
s3_client = boto3.client("s3") s3_client = boto3.client('s3')
demo = self.demo_in_path() demo = self.demo_in_path()
bucket = self.target_bucket() bucket = self.target_bucket()
print("Uploading {} to bucket {}.".format(demo, bucket)) print("Uploading {} to bucket {}.".format(demo, bucket))
s3_client.upload_file( s3_client.upload_file(
demo, demo, 'yrriban', bucket,
"yrriban", ExtraArgs={
bucket, 'ContentType': 'binary/octet-stream',
ExtraArgs={"ContentType": "binary/octet-stream", "ACL": "public-read"}, 'ACL': 'public-read'
}
) )

View file

@ -1,13 +1,13 @@
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import os import os
class Record(doomcc.doom_base.WadMap): class Record(dcc.doom_base.WadMap):
def take_action(self, parsed_args): def take_action(self, parsed_args):
os.execv( os.execv(self.dsda,
self.dsda, [self.dsda] + self.dsda_preamble() +
[self.dsda] + self.dsda_preamble() + ["-record", self.demo_out_path()], ["-record", self.demo_out_path()]
) )
def options_dict(self): def options_dict(self):

View file

@ -1,10 +1,10 @@
import doomcc.doom_base import dcc.doom_base
import pathlib import pathlib
import os import os
import time import time
class RIB(doomcc.doom_base.WadMap): class RIB(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("-s", "--secs_before", type=int, default=10) parser.add_argument("-s", "--secs_before", type=int, default=10)
@ -16,10 +16,10 @@ class RIB(doomcc.doom_base.WadMap):
demo = "" demo = ""
dt = 0 dt = 0
demodir = ( demodir = (
pathlib.Path.home() pathlib.Path.home() /
/ ".dsda-doom" ".dsda-doom" /
/ "dsda_doom_data" "dsda_doom_data" /
/ self.iwad_path.stem.lower() self.iwad_path.stem.lower()
) )
for w in self.load_order(): for w in self.load_order():
demodir = demodir.joinpath(w.stem.lower()) demodir = demodir.joinpath(w.stem.lower())
@ -41,10 +41,8 @@ class RIB(doomcc.doom_base.WadMap):
+ f"and map {parsed_args.map} (tried to look in {demodir})" + f"and map {parsed_args.map} (tried to look in {demodir})"
) )
os.execv( os.execv(self.dsda,
self.dsda, [self.dsda] + self.dsda_preamble(warp=False)
[self.dsda]
+ self.dsda_preamble(warp=False)
+ ["-playdemo", demo] + ["-playdemo", demo]
+ ["-skiptic", str(-35 * parsed_args.secs_before)], + ["-skiptic", str(-35 * parsed_args.secs_before)]
) )

View file

@ -1,12 +1,12 @@
from tkinter import messagebox from tkinter import messagebox
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import sys import sys
import wand.display import wand.display
import wand.image import wand.image
class SS(doomcc.doom_base.WadMap): class SS(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("-g", "--gravity", default="center") parser.add_argument("-g", "--gravity", default="center")
@ -22,11 +22,11 @@ class SS(doomcc.doom_base.WadMap):
height = self.thumbnail_height height = self.thumbnail_height
with wand.image.Image(width=width, height=height, pseudo="x:") as img: with wand.image.Image(width=width, height=height, pseudo="x:") as img:
img.reset_coords() img.reset_coords()
if img.width < width or img.height < height: if (img.width < width or img.height < height):
if not messagebox.askretrycancel( if not messagebox.askretrycancel(
title="Doom Command Center", title="DCC",
message=f"Image too small ({img.width}x{img.height})." message=f"Image too small ({img.width}x{img.height})." +
+ "Try again?", "Try again?"
): ):
sys.exit("Gave up trying to select an image.") sys.exit("Gave up trying to select an image.")
return False return False
@ -35,7 +35,7 @@ class SS(doomcc.doom_base.WadMap):
if not yolo: if not yolo:
wand.display.display(img) wand.display.display(img)
accepted = messagebox.askyesnocancel( accepted = messagebox.askyesnocancel(
title="Doom Command Center", message="Is this image acceptable?" title="DCC", message="Is this image acceptable?"
) )
if accepted is None: if accepted is None:
sys.exit("Gave up on image verification") sys.exit("Gave up on image verification")

View file

@ -1,5 +1,5 @@
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import sys import sys
import textwrap import textwrap
import wand.drawing import wand.drawing
@ -30,7 +30,9 @@ def draw_text(self, img, text, font_size=64, wrap_dist=0.75):
columns = len(wrapped_text) columns = len(wrapped_text)
while columns > 0: while columns > 0:
columns -= 1 columns -= 1
wrapped_text = "\n".join(textwrap.wrap(wrapped_text, columns)) wrapped_text = '\n'.join(
textwrap.wrap(wrapped_text, columns)
)
wrapped_width, _ = eval_metrics(wrapped_text) wrapped_width, _ = eval_metrics(wrapped_text)
if wrapped_width <= target_width: if wrapped_width <= target_width:
break break
@ -41,20 +43,20 @@ def draw_text(self, img, text, font_size=64, wrap_dist=0.75):
) )
textlines[idx] = wrapped_text textlines[idx] = wrapped_text
wrapped_text = "\n".join(textlines) wrapped_text = '\n'.join(textlines)
draw.text(5, int(draw.font_size) + 5, wrapped_text) draw.text(5, int(draw.font_size) + 5, wrapped_text)
draw(img) draw(img)
draw.stroke_color = wand.color.Color("none") draw.stroke_color = wand.color.Color("none")
draw.stroke_width = 0 draw.stroke_width = 0
draw.text(5, int(draw.font_size) + 5, wrapped_text) draw.text(5, int(draw.font_size)+5, wrapped_text)
draw(img) draw(img)
doomcc.config.Base.draw_text = draw_text dcc.config.Base.draw_text = draw_text
class Text(doomcc.doom_base.WadMap): class Text(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("--nomap", action="store_true") parser.add_argument("--nomap", action="store_true")
@ -72,9 +74,12 @@ class Text(doomcc.doom_base.WadMap):
text += "\n" text += "\n"
text = "{}{}".format(text, parsed_args.demotype) text = "{}{}".format(text, parsed_args.demotype)
with wand.image.Image( with wand.image.Image(
height=self.thumbnail_height, width=self.thumbnail_width height=self.thumbnail_height,
width=self.thumbnail_width
) as img: ) as img:
self.draw_text(img, text, wrap_dist=0.95) self.draw_text(
img, text, wrap_dist=0.95
)
img.trim() img.trim()
img.reset_coords() img.reset_coords()
img.save(filename=self.text_thumb_path()) img.save(filename=self.text_thumb_path())
@ -86,7 +91,7 @@ class Text(doomcc.doom_base.WadMap):
map_names = self._config.get("map_names") map_names = self._config.get("map_names")
if map_names is not None: if map_names is not None:
text = map_names.get(f"map{mapnum}") text = map_names.get(f"map{mapnum}")
if text is not None: if text != "":
return text return text
return input("Map Name? ") return input("Map Name? ")

View file

@ -1,10 +1,10 @@
import doomcc.config import dcc.config
import doomcc.doom_base import dcc.doom_base
import wand.color import wand.color
import wand.image import wand.image
class Thumb(doomcc.doom_base.WadMap): class Thumb(dcc.doom_base.WadMap):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super().get_parser(prog_name) parser = super().get_parser(prog_name)
parser.add_argument("--index", action="store_true") parser.add_argument("--index", action="store_true")
@ -17,7 +17,7 @@ class Thumb(doomcc.doom_base.WadMap):
overlay = self.thumb_overlay_path() overlay = self.thumb_overlay_path()
with ( with (
wand.image.Image(filename=base) as bi, wand.image.Image(filename=base) as bi,
wand.color.Color("transparent") as tc, wand.color.Color("transparent") as tc
): ):
with wand.image.Image(filename=text) as ti: with wand.image.Image(filename=text) as ti:
ti.border(tc, 5, 5) ti.border(tc, 5, 5)

View file

@ -1,5 +0,0 @@
import sys
from doomcc.main import main
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View file

@ -1,72 +0,0 @@
import sys
from cliff.app import App
from cliff.commandmanager import CommandManager
import doomcc.concat
import doomcc.configure
import doomcc.dsda
import doomcc.eureka
import doomcc.extract
import doomcc.fabricate
import doomcc.fetch
import doomcc.ls
import doomcc.pb
import doomcc.play
import doomcc.put
import doomcc.record
import doomcc.rib
import doomcc.ss
import doomcc.text
import doomcc.thumb
class DoomCC(App):
def __init__(self):
cm = CommandManager(None)
commands = {
"concat": doomcc.concat.Concat,
"configure": doomcc.configure.Configure,
"dsda": doomcc.dsda.DSDA,
"eureka": doomcc.eureka.Eureka,
"extract": doomcc.extract.Extract,
"fabricate": doomcc.fabricate.Fabricate,
"fetch": doomcc.fetch.Fetch,
"ls": doomcc.ls.List,
"ls demos": doomcc.ls.ListDemos,
"ls videos": doomcc.ls.ListVideos,
"pb": doomcc.pb.PB,
"play": doomcc.play.Play,
"put": doomcc.put.Put,
"record": doomcc.record.Record,
"rib": doomcc.rib.RIB,
"ss": doomcc.ss.SS,
"text": doomcc.text.Text,
"thumb": doomcc.thumb.Thumb,
}
for n, c in commands.items():
cm.add_command(n, c)
super().__init__(
description="Doom Command Center",
version="0.0.1",
command_manager=cm,
deferred_help=True,
)
def initialize_app(self, argv):
pass
def prepare_to_run_command(self, cmd):
pass
def clean_up(self, cmd, result, err):
pass
def main(argv=sys.argv[1:]):
doomcc = DoomCC()
return doomcc.run(argv)
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,11 +1,11 @@
[project] [project]
name = "doomcc" name = "dcc"
version = "0.1.0" version = "0.1.0"
description = "Doom Command Center" description = "Doom Command Center"
authors = [ authors = [
{name = "yrriban",email = "yrriban@gmail.com"} {name = "yrriban",email = "yrriban@gmail.com"}
] ]
license = "MIT" license = {text = "MIT"}
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
@ -27,10 +27,10 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[project.scripts] [project.scripts]
doomcc = "doomcc.main:main" dcc = "dcc.main:main"
[tool.poetry-pyinstaller-plugin.scripts] [tool.poetry-pyinstaller-plugin.scripts]
doomcc = { source = "doomcc/main.py", type = "onefile" } dcc = { source = "dcc/main.py", type = "onefile" }
[tool.poetry-pyinstaller-plugin.collect] [tool.poetry-pyinstaller-plugin.collect]
all = ["cliff"] all = ["cliff"]

View file

@ -1,28 +0,0 @@
import doomcc.doom_base
import os
import pathlib
import pytest
import tempfile
class Workbench:
pass
def test_demo_in_path():
w = Workbench()
w.demo_in_path = lambda: doomcc.doom_base.WadMap.demo_in_path(w)
w._file_base = lambda path: doomcc.doom_base.WadMap._file_base(w, path)
w.wad = pathlib.Path("scythe")
w.map = "01"
w.name_string = "_index"
w._name = "index"
with tempfile.TemporaryDirectory() as td:
w.demos = pathlib.Path(td)
with pytest.raises(Exception):
w.demo_in_path()
dp = pathlib.Path(td).joinpath("scythe")
os.mkdir(dp)
dp.joinpath("scythe_map01_index.lmp").touch()
assert w.demo_in_path() == dp.joinpath("scythe_map01_index.lmp")
dp.joinpath("scythe_map01_index-00028.lmp").touch()
assert w.demo_in_path() == dp.joinpath("scythe_map01_index-00028.lmp")

View file

@ -1,5 +1,5 @@
import cliff.app import cliff.app
import doomcc.play import dcc.play
import logging import logging
import os import os
import mockito.matchers import mockito.matchers
@ -26,30 +26,26 @@ def test_play(expect):
scp.mkdir() scp.mkdir()
scp.joinpath("config.toml").touch() scp.joinpath("config.toml").touch()
scp.joinpath("scythe.wad").touch() scp.joinpath("scythe.wad").touch()
doomcc.play.Play.__init__ = lambda self: None dcc.play.Play.__init__ = lambda self: None
doomcc.play.Play.get_epilog = lambda self: "" dcc.play.Play.get_epilog = lambda self: ""
rig = doomcc.play.Play() rig = dcc.play.Play()
rig._hooks = [] rig._hooks = []
parser = rig.get_parser("test_play") parser = rig.get_parser("test_play")
parsed_args = parser.parse_args(args=["--doom", td, "scythe", "01"]) parsed_args = parser.parse_args(args=["--doom", td, "scythe", "01"])
with ( with expect(os, times=1).execv(
expect(os, times=1) tdp.joinpath("dsda-doom").joinpath("exe"),
.execv( [
tdp.joinpath("dsda-doom").joinpath("exe"), tdp.joinpath("dsda-doom").joinpath("exe"),
[ "-iwad",
tdp.joinpath("dsda-doom").joinpath("exe"), tdp.joinpath("iwads").joinpath("DOOM2.WAD"),
"-iwad", "-file",
tdp.joinpath("iwads").joinpath("DOOM2.WAD"), tdp.joinpath("pwads").joinpath("scythe").joinpath("scythe.wad"),
"-file", "-complevel",
tdp.joinpath("pwads").joinpath("scythe").joinpath("scythe.wad"), "2",
"-complevel", "-skill",
"2", "4",
"-skill", "-warp",
"4", "01",
"-warp", ]
"01", ).thenReturn(None):
],
)
.thenReturn(None)
):
assert rig.run(parsed_args) is None assert rig.run(parsed_args) is None