mirror of
https://github.com/ciromattia/kcc
synced 2026-04-23 09:28:59 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a822dfa3ae | ||
|
|
70de379987 | ||
|
|
16e275bb1f | ||
|
|
b225de7b97 | ||
|
|
4a89446914 | ||
|
|
64521de577 | ||
|
|
38b14fd734 | ||
|
|
4fa72780a1 | ||
|
|
c979486e28 | ||
|
|
03bd67cf2f |
@@ -1,5 +1,12 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
#### 5.6.1:
|
||||||
|
* Fix pillow backwards compatibility, add mozjpeg-lossless-optimization to setup.py by @corylk in #461
|
||||||
|
* fix in fedora: 7z doesn't support rar archives, use unrar by @AlicesReflexion in #370
|
||||||
|
* Using communicate instead of terminate by @catsout in #459
|
||||||
|
* use copyfile and delete instead of shutil.move fix #386 by @StudioEtrange in #387
|
||||||
|
|
||||||
|
|
||||||
#### 5.6.0:
|
#### 5.6.0:
|
||||||
* Fix Docker 7z missing [darodi/kcc#31](https://github.com/darodi/kcc/issues/31), thanks [@darodi](https://github.com/darodi)
|
* Fix Docker 7z missing [darodi/kcc#31](https://github.com/darodi/kcc/issues/31), thanks [@darodi](https://github.com/darodi)
|
||||||
* update to python 3.11, thanks [@darodi](https://github.com/darodi)
|
* update to python 3.11, thanks [@darodi](https://github.com/darodi)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[](https://github.com/ciromattia/kcc/releases)
|
[](https://github.com/ciromattia/kcc/releases)
|
||||||
[](https://test.pypi.org/project/KindleComicConverterDarodi/)
|
[](https://test.pypi.org/project/KindleComicConverterDarodi/)
|
||||||
|
|
||||||
[](https://github.com/darodi/kcc/pkgs/container/kcc)
|
[](https://github.com/ciromattia/kcc/pkgs/container/kcc)
|
||||||
|
|
||||||
[//]: # ([](https://aur.archlinux.org/packages/kcc-beta))
|
[//]: # ([](https://aur.archlinux.org/packages/kcc-beta))
|
||||||
|
|
||||||
@@ -286,6 +286,10 @@ After completed conversion, you should find ready file alongside the original in
|
|||||||
Please check [our wiki](https://github.com/ciromattia/kcc/wiki/) for more details.
|
Please check [our wiki](https://github.com/ciromattia/kcc/wiki/) for more details.
|
||||||
|
|
||||||
CLI version of **KCC** is intended for power users. It allows using options that might not be compatible and decrease the quality of output.
|
CLI version of **KCC** is intended for power users. It allows using options that might not be compatible and decrease the quality of output.
|
||||||
|
CLI version has reduced dependencies, on Debian based distributions this commands should install all needed dependencies:
|
||||||
|
```
|
||||||
|
sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugify
|
||||||
|
```
|
||||||
|
|
||||||
### Profiles:
|
### Profiles:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = '5.6.0'
|
__version__ = '5.6.1'
|
||||||
__license__ = 'ISC'
|
__license__ = 'ISC'
|
||||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from re import sub
|
|||||||
from stat import S_IWRITE, S_IREAD, S_IEXEC
|
from stat import S_IWRITE, S_IREAD, S_IEXEC
|
||||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||||
from shutil import move, copytree, rmtree
|
from shutil import move, copytree, rmtree, copyfile
|
||||||
from optparse import OptionParser, OptionGroup
|
from optparse import OptionParser, OptionGroup
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -302,6 +302,11 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
|||||||
else:
|
else:
|
||||||
f.writelines(["<meta name=\"orientation-lock\" content=\"portrait\"/>\n",
|
f.writelines(["<meta name=\"orientation-lock\" content=\"portrait\"/>\n",
|
||||||
"<meta name=\"region-mag\" content=\"true\"/>\n"])
|
"<meta name=\"region-mag\" content=\"true\"/>\n"])
|
||||||
|
elif options.supportSyntheticSpread:
|
||||||
|
f.writelines([
|
||||||
|
"<meta property=\"rendition:spread\">landscape</meta>\n",
|
||||||
|
"<meta property=\"rendition:layout\">pre-paginated</meta>\n"
|
||||||
|
])
|
||||||
else:
|
else:
|
||||||
f.writelines(["<meta property=\"rendition:orientation\">portrait</meta>\n",
|
f.writelines(["<meta property=\"rendition:orientation\">portrait</meta>\n",
|
||||||
"<meta property=\"rendition:spread\">portrait</meta>\n",
|
"<meta property=\"rendition:spread\">portrait</meta>\n",
|
||||||
@@ -334,38 +339,64 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
|||||||
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
|
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
|
||||||
mt + "\"/>\n")
|
mt + "\"/>\n")
|
||||||
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
|
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
|
||||||
|
|
||||||
|
|
||||||
|
def pageSpreadProperty(pageside):
|
||||||
|
if options.iskindle:
|
||||||
|
return "linear=\"yes\" properties=\"page-spread-%s\"" % pageside
|
||||||
|
elif options.isKobo:
|
||||||
|
return "properties=\"rendition:page-spread-%s\"" % pageside
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
if options.righttoleft:
|
if options.righttoleft:
|
||||||
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
|
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
else:
|
else:
|
||||||
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
if options.iskindle:
|
if options.iskindle or options.supportSyntheticSpread:
|
||||||
for entry in reflist:
|
for entry in reflist:
|
||||||
if options.righttoleft:
|
if options.righttoleft:
|
||||||
if entry.endswith("-b"):
|
if entry.endswith("-b"):
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" linear=\"yes\" properties=\"page-spread-right\"/>\n")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("right"))
|
||||||
|
)
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
elif entry.endswith("-c"):
|
elif entry.endswith("-c"):
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" linear=\"yes\" properties=\"page-spread-left\"/>\n")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("left"))
|
||||||
|
)
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
else:
|
else:
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" linear=\"yes\" properties=\"page-spread-" +
|
f.write(
|
||||||
pageside + "\"/>\n")
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty(pageside))
|
||||||
|
)
|
||||||
if pageside == "right":
|
if pageside == "right":
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
else:
|
else:
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
else:
|
else:
|
||||||
if entry.endswith("-b"):
|
if entry.endswith("-b"):
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" linear=\"yes\" properties=\"page-spread-left\"/>\n")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("left"))
|
||||||
|
)
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
elif entry.endswith("-c"):
|
elif entry.endswith("-c"):
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" linear=\"yes\" properties=\"page-spread-right\"/>\n")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("right"))
|
||||||
|
)
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
else:
|
else:
|
||||||
f.write("<itemref idref=\"page_" + entry + "\" linear=\"yes\" properties=\"page-spread-" +
|
f.write(
|
||||||
pageside + "\"/>\n")
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty(pageside))
|
||||||
|
)
|
||||||
if pageside == "right":
|
if pageside == "right":
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
else:
|
else:
|
||||||
@@ -557,9 +588,9 @@ def imgFileProcessing(work):
|
|||||||
for i in workImg.payload:
|
for i in workImg.payload:
|
||||||
img = image.ComicPage(opt, *i)
|
img = image.ComicPage(opt, *i)
|
||||||
if opt.cropping == 2 and not opt.webtoon:
|
if opt.cropping == 2 and not opt.webtoon:
|
||||||
img.cropPageNumber(opt.croppingp)
|
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
||||||
if opt.cropping > 0 and not opt.webtoon:
|
if opt.cropping > 0 and not opt.webtoon:
|
||||||
img.cropMargin(opt.croppingp)
|
img.cropMargin(opt.croppingp, opt.croppingm)
|
||||||
img.autocontrastImage()
|
img.autocontrastImage()
|
||||||
img.resizeImage()
|
img.resizeImage()
|
||||||
if opt.forcepng and not opt.forcecolor:
|
if opt.forcepng and not opt.forcecolor:
|
||||||
@@ -951,6 +982,8 @@ def makeParser():
|
|||||||
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
|
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
|
||||||
processingOptions.add_option("--cp", "--croppingpower", type="float", dest="croppingp", default="1.0",
|
processingOptions.add_option("--cp", "--croppingpower", type="float", dest="croppingp", default="1.0",
|
||||||
help="Set cropping power [Default=1.0]")
|
help="Set cropping power [Default=1.0]")
|
||||||
|
processingOptions.add_option("--cm", "--croppingminimum", type="float", dest="croppingm", default="0.0",
|
||||||
|
help="Set cropping minimum area ratio [Default=0.0]")
|
||||||
processingOptions.add_option("--blackborders", action="store_true", dest="black_borders", default=False,
|
processingOptions.add_option("--blackborders", action="store_true", dest="black_borders", default=False,
|
||||||
help="Disable autodetection and force black borders")
|
help="Disable autodetection and force black borders")
|
||||||
processingOptions.add_option("--whiteborders", action="store_true", dest="white_borders", default=False,
|
processingOptions.add_option("--whiteborders", action="store_true", dest="white_borders", default=False,
|
||||||
@@ -982,6 +1015,7 @@ def makeParser():
|
|||||||
def checkOptions(options):
|
def checkOptions(options):
|
||||||
options.panelview = True
|
options.panelview = True
|
||||||
options.iskindle = False
|
options.iskindle = False
|
||||||
|
options.isKobo = False
|
||||||
options.bordersColor = None
|
options.bordersColor = None
|
||||||
options.keep_epub = False
|
options.keep_epub = False
|
||||||
if options.format == 'EPUB-200MB':
|
if options.format == 'EPUB-200MB':
|
||||||
@@ -993,6 +1027,7 @@ def checkOptions(options):
|
|||||||
options.keep_epub = True
|
options.keep_epub = True
|
||||||
options.format = 'MOBI'
|
options.format = 'MOBI'
|
||||||
options.kfx = False
|
options.kfx = False
|
||||||
|
options.supportSyntheticSpread = False
|
||||||
if options.format == 'Auto':
|
if options.format == 'Auto':
|
||||||
if options.profile in ['K1', 'K2', 'K34', 'K578', 'KPW', 'KPW5', 'KV', 'KO', 'K11', 'KS']:
|
if options.profile in ['K1', 'K2', 'K34', 'K578', 'KPW', 'KPW5', 'KV', 'KO', 'K11', 'KS']:
|
||||||
options.format = 'MOBI'
|
options.format = 'MOBI'
|
||||||
@@ -1003,6 +1038,12 @@ def checkOptions(options):
|
|||||||
options.format = 'CBZ'
|
options.format = 'CBZ'
|
||||||
if options.profile in ['K1', 'K2', 'K34', 'K578', 'KPW', 'KPW5', 'KV', 'KO', 'K11', 'KS']:
|
if options.profile in ['K1', 'K2', 'K34', 'K578', 'KPW', 'KPW5', 'KV', 'KO', 'K11', 'KS']:
|
||||||
options.iskindle = True
|
options.iskindle = True
|
||||||
|
elif options.profile in ['OTHER', 'KoMT', 'KoG', 'KoGHD', 'KoA', 'KoAHD', 'KoAH2O', 'KoAO', 'KoN', 'KoC', 'KoL', 'KoF', 'KoS', 'KoE']:
|
||||||
|
options.isKobo = True
|
||||||
|
# Other Kobo devices probably support synthetic spreads as well, but
|
||||||
|
# they haven't been tested.
|
||||||
|
if options.profile in ['KoF']:
|
||||||
|
options.supportSyntheticSpread = True
|
||||||
if options.white_borders:
|
if options.white_borders:
|
||||||
options.bordersColor = 'white'
|
options.bordersColor = 'white'
|
||||||
if options.black_borders:
|
if options.black_borders:
|
||||||
@@ -1152,7 +1193,12 @@ def makeBook(source, qtgui=None):
|
|||||||
else:
|
else:
|
||||||
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
||||||
makeZIP(tome + '_comic', tome, True)
|
makeZIP(tome + '_comic', tome, True)
|
||||||
move(tome + '_comic.zip', filepath[-1])
|
copyfile(tome + '_comic.zip', filepath[-1])
|
||||||
|
try:
|
||||||
|
os.remove(tome + '_comic.zip')
|
||||||
|
except FileNotFoundError:
|
||||||
|
# newly temporary created file is not found. It might have been already deleted
|
||||||
|
pass
|
||||||
rmtree(tome, True)
|
rmtree(tome, True)
|
||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('tick')
|
GUI.progressBarTick.emit('tick')
|
||||||
@@ -1224,7 +1270,8 @@ def makeMOBIWorker(item):
|
|||||||
if kindlegenErrorCode > 0:
|
if kindlegenErrorCode > 0:
|
||||||
break
|
break
|
||||||
if ":I1036: Mobi file built successfully" in line:
|
if ":I1036: Mobi file built successfully" in line:
|
||||||
output.terminate()
|
output.communicate()
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
# ERROR: EPUB too big
|
# ERROR: EPUB too big
|
||||||
kindlegenErrorCode = 23026
|
kindlegenErrorCode = 23026
|
||||||
|
|||||||
@@ -39,7 +39,14 @@ class ComicArchive:
|
|||||||
break
|
break
|
||||||
process.communicate()
|
process.communicate()
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise OSError('Archive is corrupted or encrypted.')
|
process = Popen('unrar l -y -p1 "' + self.filepath + '"', stderr=STDOUT, stdout=PIPE, stdin=PIPE, shell=True)
|
||||||
|
for line in process.stdout:
|
||||||
|
if b'Details: ' in line:
|
||||||
|
self.type = line.rstrip().decode().split(' ')[1].upper()
|
||||||
|
print(self.type)
|
||||||
|
break
|
||||||
|
if(self.type != 'RAR'):
|
||||||
|
raise OSError('Archive is corrupted or encrypted.')
|
||||||
elif self.type not in ['7Z', 'RAR', 'RAR5', 'ZIP']:
|
elif self.type not in ['7Z', 'RAR', 'RAR5', 'ZIP']:
|
||||||
raise OSError('Unsupported archive format.')
|
raise OSError('Unsupported archive format.')
|
||||||
|
|
||||||
@@ -50,7 +57,11 @@ class ComicArchive:
|
|||||||
self.filepath + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
|
self.filepath + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise OSError('Failed to extract archive.')
|
process = Popen('unrar x -y -x__MACOSX -x.DS_Store -xthumbs.db -xThumbs.db "' + self.filepath + '" "' +
|
||||||
|
targetdir + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
|
||||||
|
process.communicate()
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise OSError('Failed to extract archive.')
|
||||||
tdir = os.listdir(targetdir)
|
tdir = os.listdir(targetdir)
|
||||||
if 'ComicInfo.xml' in tdir:
|
if 'ComicInfo.xml' in tdir:
|
||||||
tdir.remove('ComicInfo.xml')
|
tdir.remove('ComicInfo.xml')
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ class ComicPageParser:
|
|||||||
self.image = Image.open(os.path.join(source[0], source[1])).convert('RGB')
|
self.image = Image.open(os.path.join(source[0], source[1])).convert('RGB')
|
||||||
self.color = self.colorCheck()
|
self.color = self.colorCheck()
|
||||||
self.fill = self.fillCheck()
|
self.fill = self.fillCheck()
|
||||||
self.splitCheck()
|
|
||||||
# backwards compatibility for Pillow >9.1.0
|
# backwards compatibility for Pillow >9.1.0
|
||||||
if not hasattr(Image, 'Resampling'):
|
if not hasattr(Image, 'Resampling'):
|
||||||
Image.Resampling = Image
|
Image.Resampling = Image
|
||||||
|
self.splitCheck()
|
||||||
|
|
||||||
def getImageHistogram(self, image):
|
def getImageHistogram(self, image):
|
||||||
histogram = image.histogram()
|
histogram = image.histogram()
|
||||||
@@ -350,7 +350,13 @@ class ComicPage:
|
|||||||
)
|
)
|
||||||
return bbox
|
return bbox
|
||||||
|
|
||||||
def cropPageNumber(self, power):
|
def maybeCrop(self, box, minimum):
|
||||||
|
box_area = (box[2] - box[0]) * (box[3] - box[1])
|
||||||
|
image_area = self.image.size[0] * self.image.size[1]
|
||||||
|
if (box_area / image_area) >= minimum:
|
||||||
|
self.image = self.image.crop(box)
|
||||||
|
|
||||||
|
def cropPageNumber(self, power, minimum):
|
||||||
if self.fill != 'white':
|
if self.fill != 'white':
|
||||||
tmptmg = self.image.convert(mode='L')
|
tmptmg = self.image.convert(mode='L')
|
||||||
else:
|
else:
|
||||||
@@ -359,16 +365,18 @@ class ComicPage:
|
|||||||
tmptmg = tmptmg.filter(ImageFilter.MinFilter(size=3))
|
tmptmg = tmptmg.filter(ImageFilter.MinFilter(size=3))
|
||||||
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=5))
|
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=5))
|
||||||
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
|
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
|
||||||
self.image = self.image.crop(tmptmg.getbbox()) if tmptmg.getbbox() else self.image
|
if tmptmg.getbbox():
|
||||||
|
self.maybeCrop(tmptmg.getbbox(), minimum)
|
||||||
|
|
||||||
def cropMargin(self, power):
|
def cropMargin(self, power, minimum):
|
||||||
if self.fill != 'white':
|
if self.fill != 'white':
|
||||||
tmptmg = self.image.convert(mode='L')
|
tmptmg = self.image.convert(mode='L')
|
||||||
else:
|
else:
|
||||||
tmptmg = ImageOps.invert(self.image.convert(mode='L'))
|
tmptmg = ImageOps.invert(self.image.convert(mode='L'))
|
||||||
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=3))
|
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=3))
|
||||||
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
|
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
|
||||||
self.image = self.image.crop(self.getBoundingBox(tmptmg)) if tmptmg.getbbox() else self.image
|
if tmptmg.getbbox():
|
||||||
|
self.maybeCrop(self.getBoundingBox(tmptmg), minimum)
|
||||||
|
|
||||||
|
|
||||||
class Cover:
|
class Cover:
|
||||||
@@ -381,10 +389,10 @@ class Cover:
|
|||||||
else:
|
else:
|
||||||
self.tomeid = tomeid
|
self.tomeid = tomeid
|
||||||
self.image = Image.open(source)
|
self.image = Image.open(source)
|
||||||
self.process()
|
|
||||||
# backwards compatibility for Pillow >9.1.0
|
# backwards compatibility for Pillow >9.1.0
|
||||||
if not hasattr(Image, 'Resampling'):
|
if not hasattr(Image, 'Resampling'):
|
||||||
Image.Resampling = Image
|
Image.Resampling = Image
|
||||||
|
self.process()
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
self.image = self.image.convert('RGB')
|
self.image = self.image.convert('RGB')
|
||||||
|
|||||||
Reference in New Issue
Block a user