mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
GwR revisions
This commit is contained in:
parent
c1293b5a94
commit
e17b085c9f
@ -237,8 +237,6 @@ class ITUNES(DevicePlugin):
|
|||||||
(new_book.title, new_book.author))
|
(new_book.title, new_book.author))
|
||||||
booklists[0].append(new_book)
|
booklists[0].append(new_book)
|
||||||
|
|
||||||
self.update_list = []
|
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self._dump_booklist(booklists[0],header='after',indent=2)
|
self._dump_booklist(booklists[0],header='after',indent=2)
|
||||||
self._dump_cached_books(header='after',indent=2)
|
self._dump_cached_books(header='after',indent=2)
|
||||||
@ -414,7 +412,7 @@ class ITUNES(DevicePlugin):
|
|||||||
self.ejected = True
|
self.ejected = True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._discover_manual_sync_mode()
|
self._discover_manual_sync_mode(wait = 2 if self.initial_status == 'launched' else 0)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def can_handle_windows(self, device_id, debug=False):
|
def can_handle_windows(self, device_id, debug=False):
|
||||||
@ -676,25 +674,18 @@ class ITUNES(DevicePlugin):
|
|||||||
self.log.info("ITUNES.remove_books_from_metadata()")
|
self.log.info("ITUNES.remove_books_from_metadata()")
|
||||||
for path in paths:
|
for path in paths:
|
||||||
self._dump_cached_book(self.cached_books[path], indent=2)
|
self._dump_cached_book(self.cached_books[path], indent=2)
|
||||||
if self.cached_books[path]['lib_book']:
|
|
||||||
# Remove from the booklist
|
|
||||||
for i,book in enumerate(booklists[0]):
|
|
||||||
print "book.uuid: %s" % book.uuid
|
|
||||||
print "self.cached_books[path]['uuid']: %s" % self.cached_books[path]['uuid']
|
|
||||||
if book.uuid == self.cached_books[path]['uuid']:
|
|
||||||
booklists[0].pop(i)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.log.error(" '%s' not found in self.cached_book" % path)
|
|
||||||
|
|
||||||
# Remove from cached_books
|
# Purge the booklist, self.cached_books
|
||||||
self.cached_books.pop(path)
|
for i,bl_book in enumerate(booklists[0]):
|
||||||
if DEBUG:
|
if bl_book.uuid == self.cached_books[path]['uuid']:
|
||||||
self.log.info(" removing '%s' from self.cached_books" % path)
|
# Remove from booklists[0]
|
||||||
self._dump_cached_books(header='remove_books_from_metadata()',indent=2)
|
booklists[0].pop(i)
|
||||||
self._dump_booklist(booklists[0], header='remove_books_from_metadata()',indent=2)
|
|
||||||
else:
|
for cb in self.cached_books:
|
||||||
self.log.warning(" skipping purchased book, can't remove via automation interface")
|
if self.cached_books[cb]['uuid'] == self.cached_books[path]['uuid']:
|
||||||
|
self.cached_books.pop(cb)
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
def reset(self, key='-1', log_packets=False, report_progress=None,
|
def reset(self, key='-1', log_packets=False, report_progress=None,
|
||||||
detected_device=None) :
|
detected_device=None) :
|
||||||
@ -749,6 +740,7 @@ class ITUNES(DevicePlugin):
|
|||||||
details='\n'.join(self.problem_titles), level=UserFeedback.WARN)
|
details='\n'.join(self.problem_titles), level=UserFeedback.WARN)
|
||||||
self.problem_titles = []
|
self.problem_titles = []
|
||||||
self.problem_msg = None
|
self.problem_msg = None
|
||||||
|
self.update_list = []
|
||||||
|
|
||||||
def total_space(self, end_session=True):
|
def total_space(self, end_session=True):
|
||||||
"""
|
"""
|
||||||
@ -1152,6 +1144,8 @@ class ITUNES(DevicePlugin):
|
|||||||
'''
|
'''
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" ITUNES._discover_manual_sync_mode()")
|
self.log.info(" ITUNES._discover_manual_sync_mode()")
|
||||||
|
if wait:
|
||||||
|
time.sleep(wait)
|
||||||
if isosx:
|
if isosx:
|
||||||
connected_device = self.sources['iPod']
|
connected_device = self.sources['iPod']
|
||||||
dev_books = None
|
dev_books = None
|
||||||
@ -1165,8 +1159,8 @@ class ITUNES(DevicePlugin):
|
|||||||
|
|
||||||
if len(dev_books):
|
if len(dev_books):
|
||||||
first_book = dev_books[0]
|
first_book = dev_books[0]
|
||||||
#if DEBUG:
|
if DEBUG:
|
||||||
#self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist()))
|
self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist()))
|
||||||
try:
|
try:
|
||||||
first_book.bpm.set(0)
|
first_book.bpm.set(0)
|
||||||
self.manual_sync_mode = True
|
self.manual_sync_mode = True
|
||||||
@ -1184,8 +1178,6 @@ class ITUNES(DevicePlugin):
|
|||||||
self.manual_sync_mode = False
|
self.manual_sync_mode = False
|
||||||
|
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
if wait:
|
|
||||||
time.sleep(wait)
|
|
||||||
connected_device = self.sources['iPod']
|
connected_device = self.sources['iPod']
|
||||||
device = self.iTunes.sources.ItemByName(connected_device)
|
device = self.iTunes.sources.ItemByName(connected_device)
|
||||||
|
|
||||||
@ -1294,18 +1286,35 @@ class ITUNES(DevicePlugin):
|
|||||||
|
|
||||||
self.log.info()
|
self.log.info()
|
||||||
|
|
||||||
def _dump_hex(self, src, length=16):
|
def _dump_epub_metadata(self, fpath):
|
||||||
'''
|
'''
|
||||||
'''
|
'''
|
||||||
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
|
self.log.info(" ITUNES.__get_epub_metadata()")
|
||||||
N=0; result=''
|
title = None
|
||||||
while src:
|
author = None
|
||||||
s,src = src[:length],src[length:]
|
timestamp = None
|
||||||
hexa = ' '.join(["%02X"%ord(x) for x in s])
|
zf = ZipFile(fpath,'r')
|
||||||
s = s.translate(FILTER)
|
fnames = zf.namelist()
|
||||||
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
|
opf = [x for x in fnames if '.opf' in x][0]
|
||||||
N+=length
|
if opf:
|
||||||
print result
|
opf_raw = cStringIO.StringIO(zf.read(opf)).getvalue()
|
||||||
|
soup = BeautifulSoup(opf_raw)
|
||||||
|
title = soup.find('dc:title').renderContents()
|
||||||
|
author = soup.find('dc:creator').renderContents()
|
||||||
|
ts = soup.find('meta',attrs={'name':'calibre:timestamp'})
|
||||||
|
if ts:
|
||||||
|
# Touch existing calibre timestamp
|
||||||
|
timestamp = ts['content']
|
||||||
|
|
||||||
|
if not title or not author:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.error(" couldn't extract title/author from %s in %s" % (opf,fpath))
|
||||||
|
self.log.error(" title: %s author: %s timestamp: %s" % (title, author, timestamp))
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.error(" can't find .opf in %s" % fpath)
|
||||||
|
zf.close()
|
||||||
|
return (title, author, timestamp)
|
||||||
|
|
||||||
def _dump_files(self, files, header=None,indent=0):
|
def _dump_files(self, files, header=None,indent=0):
|
||||||
if header:
|
if header:
|
||||||
@ -1319,6 +1328,19 @@ class ITUNES(DevicePlugin):
|
|||||||
self.log.info(" %s%s" % (' '*indent,file.name))
|
self.log.info(" %s%s" % (' '*indent,file.name))
|
||||||
self.log.info()
|
self.log.info()
|
||||||
|
|
||||||
|
def _dump_hex(self, src, length=16):
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
|
||||||
|
N=0; result=''
|
||||||
|
while src:
|
||||||
|
s,src = src[:length],src[length:]
|
||||||
|
hexa = ' '.join(["%02X"%ord(x) for x in s])
|
||||||
|
s = s.translate(FILTER)
|
||||||
|
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
|
||||||
|
N+=length
|
||||||
|
print result
|
||||||
|
|
||||||
def _dump_library_books(self, library_books):
|
def _dump_library_books(self, library_books):
|
||||||
'''
|
'''
|
||||||
'''
|
'''
|
||||||
@ -1615,30 +1637,6 @@ class ITUNES(DevicePlugin):
|
|||||||
self.log.error(" no iPad|Books playlist found")
|
self.log.error(" no iPad|Books playlist found")
|
||||||
return pl
|
return pl
|
||||||
|
|
||||||
def _get_epub_metadata(self, fpath):
|
|
||||||
'''
|
|
||||||
Return the original title and author from the .OPF file in the epub bundle
|
|
||||||
'''
|
|
||||||
self.log.info(" ITUNES.__get_epub_metadata()")
|
|
||||||
title = None
|
|
||||||
author = None
|
|
||||||
zf = ZipFile(fpath,'r')
|
|
||||||
fnames = zf.namelist()
|
|
||||||
opf = [x for x in fnames if '.opf' in x][0]
|
|
||||||
if opf:
|
|
||||||
opf_raw = cStringIO.StringIO(zf.read(opf)).getvalue()
|
|
||||||
soup = BeautifulSoup(opf_raw)
|
|
||||||
title = soup.find('dc:title').renderContents()
|
|
||||||
author = soup.find('dc:creator').renderContents()
|
|
||||||
if not title or not author:
|
|
||||||
if DEBUG:
|
|
||||||
self.log.error(" couldn't extract title/author from %s in %s" % (opf,fpath))
|
|
||||||
self.log.error(" title: %s author: %s" % (title, author))
|
|
||||||
else:
|
|
||||||
if DEBUG:
|
|
||||||
self.log.error(" can't find .opf in %s" % fpath)
|
|
||||||
return title, author
|
|
||||||
|
|
||||||
def _get_fpath(self,file, metadata, update_md=False):
|
def _get_fpath(self,file, metadata, update_md=False):
|
||||||
'''
|
'''
|
||||||
If the database copy will be deleted after upload, we have to
|
If the database copy will be deleted after upload, we have to
|
||||||
@ -1652,13 +1650,17 @@ class ITUNES(DevicePlugin):
|
|||||||
if not getattr(fpath, 'deleted_after_upload', False):
|
if not getattr(fpath, 'deleted_after_upload', False):
|
||||||
if getattr(file, 'orig_file_path', None) is not None:
|
if getattr(file, 'orig_file_path', None) is not None:
|
||||||
fpath = file.orig_file_path
|
fpath = file.orig_file_path
|
||||||
if update_md:
|
|
||||||
self._update_epub_metadata(fpath, metadata)
|
|
||||||
elif getattr(file, 'name', None) is not None:
|
elif getattr(file, 'name', None) is not None:
|
||||||
fpath = file.name
|
fpath = file.name
|
||||||
else:
|
else:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" file will be deleted after upload")
|
self.log.info(" file will be deleted after upload")
|
||||||
|
if update_md:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" metadata before rewrite: '{0[0]}' '{0[1]}' '{0[2]}'".format(self._dump_epub_metadata(fpath)))
|
||||||
|
self._update_epub_metadata(fpath, metadata)
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" metadata after rewrite: '{0[0]}' '{0[1]}' '{0[2]}'".format(self._dump_epub_metadata(fpath)))
|
||||||
return fpath
|
return fpath
|
||||||
|
|
||||||
def _get_library_books(self):
|
def _get_library_books(self):
|
||||||
@ -1843,10 +1845,10 @@ class ITUNES(DevicePlugin):
|
|||||||
self.log.info( "ITUNES:open(): Launching iTunes" )
|
self.log.info( "ITUNES:open(): Launching iTunes" )
|
||||||
self.iTunes = iTunes= appscript.app('iTunes', hide=True)
|
self.iTunes = iTunes= appscript.app('iTunes', hide=True)
|
||||||
iTunes.run()
|
iTunes.run()
|
||||||
initial_status = 'launched'
|
self.initial_status = 'launched'
|
||||||
else:
|
else:
|
||||||
self.iTunes = appscript.app('iTunes')
|
self.iTunes = appscript.app('iTunes')
|
||||||
initial_status = 'already running'
|
self.initial_status = 'already running'
|
||||||
|
|
||||||
# Read the current storage path for iTunes media
|
# Read the current storage path for iTunes media
|
||||||
cmd = "defaults read com.apple.itunes NSNavLastRootDirectory"
|
cmd = "defaults read com.apple.itunes NSNavLastRootDirectory"
|
||||||
@ -1860,7 +1862,7 @@ class ITUNES(DevicePlugin):
|
|||||||
self.log.error(" media_dir: %s" % media_dir)
|
self.log.error(" media_dir: %s" % media_dir)
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" [OSX %s - %s (%s), driver version %d.%d.%d]" %
|
self.log.info(" [OSX %s - %s (%s), driver version %d.%d.%d]" %
|
||||||
(self.iTunes.name(), self.iTunes.version(), initial_status,
|
(self.iTunes.name(), self.iTunes.version(), self.initial_status,
|
||||||
self.version[0],self.version[1],self.version[2]))
|
self.version[0],self.version[1],self.version[2]))
|
||||||
self.log.info(" iTunes_media: %s" % self.iTunes_media)
|
self.log.info(" iTunes_media: %s" % self.iTunes_media)
|
||||||
if iswindows:
|
if iswindows:
|
||||||
@ -1872,7 +1874,7 @@ class ITUNES(DevicePlugin):
|
|||||||
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
self.iTunes.Windows[0].Minimized = True
|
self.iTunes.Windows[0].Minimized = True
|
||||||
initial_status = 'launched'
|
self.initial_status = 'launched'
|
||||||
|
|
||||||
# Read the current storage path for iTunes media from the XML file
|
# Read the current storage path for iTunes media from the XML file
|
||||||
with open(self.iTunes.LibraryXMLPath, 'r') as xml:
|
with open(self.iTunes.LibraryXMLPath, 'r') as xml:
|
||||||
@ -1889,7 +1891,7 @@ class ITUNES(DevicePlugin):
|
|||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" [Windows %s - %s (%s), driver version %d.%d.%d]" %
|
self.log.info(" [Windows %s - %s (%s), driver version %d.%d.%d]" %
|
||||||
(self.iTunes.Windows[0].name, self.iTunes.Version, initial_status,
|
(self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status,
|
||||||
self.version[0],self.version[1],self.version[2]))
|
self.version[0],self.version[1],self.version[2]))
|
||||||
self.log.info(" iTunes_media: %s" % self.iTunes_media)
|
self.log.info(" iTunes_media: %s" % self.iTunes_media)
|
||||||
|
|
||||||
@ -2061,61 +2063,31 @@ class ITUNES(DevicePlugin):
|
|||||||
|
|
||||||
def _update_epub_metadata(self, fpath, metadata):
|
def _update_epub_metadata(self, fpath, metadata):
|
||||||
'''
|
'''
|
||||||
Refresh metadata in database epub to force iBooks to recache
|
|
||||||
'''
|
'''
|
||||||
self.log.info(" ITUNES._update_epub_metadata()")
|
self.log.info(" ITUNES._update_epub_metadata()")
|
||||||
|
|
||||||
# Refresh epub metadata
|
# Refresh epub metadata
|
||||||
with open(fpath,'r+b') as zfo:
|
with open(fpath,'r+b') as zfo:
|
||||||
|
|
||||||
|
# Touch the timestamp to force a recache
|
||||||
if metadata.timestamp:
|
if metadata.timestamp:
|
||||||
#old_ts = strptime(metadata.timestamp,"%Y-%m-%dT%H:%M:%S.%f+00:00")
|
|
||||||
old_ts = metadata.timestamp
|
old_ts = metadata.timestamp
|
||||||
metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
|
metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
|
||||||
old_ts.minute, old_ts.second, old_ts.microsecond+1)
|
old_ts.minute, old_ts.second, old_ts.microsecond+1, old_ts.tzinfo)
|
||||||
else:
|
else:
|
||||||
metadata.timestamp = isoformat(now())
|
metadata.timestamp = isoformat(now())
|
||||||
|
|
||||||
if iswindows:
|
# Tweak the author if 'News' in tags for friendlier display in iBooks
|
||||||
# This hack compensates for the Windows automation interface not allowing
|
if _('News') in metadata.tags:
|
||||||
# us to update Category after the fact. By removing the tags, we should be
|
if metadata.title.find('[') > 0:
|
||||||
# able to set the Category after the upload.
|
metadata.title = metadata.title[:metadata.title.find('[')-1]
|
||||||
if metadata.series:
|
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||||
metadata.tags = None
|
metadata.author = metadata.authors = [date_as_author]
|
||||||
|
sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', metadata.title).rstrip()
|
||||||
|
metadata.author_sort = '%s %s' % (sort_author, strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
if DEBUG:
|
|
||||||
self.log.info(" updating to '%s' by %s" % (metadata.title, metadata.authors[0]))
|
|
||||||
set_metadata(zfo,metadata)
|
set_metadata(zfo,metadata)
|
||||||
|
|
||||||
if False:
|
|
||||||
# This code operates directly on the OPF file, obsolete since we're rewriting md
|
|
||||||
zf = ZipFile(fpath,'r')
|
|
||||||
fnames = zf.namelist()
|
|
||||||
opf = [x for x in fnames if '.opf' in x][0]
|
|
||||||
if opf:
|
|
||||||
opf_raw = cStringIO.StringIO(zf.read(opf)).getvalue()
|
|
||||||
soup = BeautifulSoup(opf_raw)
|
|
||||||
md = soup.find('metadata')
|
|
||||||
ts = md.find('meta',attrs={'name':'calibre:timestamp'})
|
|
||||||
if ts:
|
|
||||||
# Touch existing calibre timestamp
|
|
||||||
timestamp = ts['content']
|
|
||||||
old_ts = strptime(timestamp,"%Y-%m-%dT%H:%M:%S.%f+00:00")
|
|
||||||
new_ts = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
|
|
||||||
old_ts.minute, old_ts.second, old_ts.microsecond+1)
|
|
||||||
ts['content'] = new_ts.strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")
|
|
||||||
else:
|
|
||||||
# Create new calibre timestamp
|
|
||||||
ts = Tag(soup,'meta')
|
|
||||||
ts['name'] = 'calibre:timestamp'
|
|
||||||
ts['content'] = isoformat(now())
|
|
||||||
md.insert(len(md),ts)
|
|
||||||
zfo = open(fpath,'r+b')
|
|
||||||
safe_replace(zfo, opf, cStringIO.StringIO(soup.renderContents()))
|
|
||||||
|
|
||||||
else:
|
|
||||||
if DEBUG:
|
|
||||||
self.log.error(" can't find .opf in %s" % fpath)
|
|
||||||
|
|
||||||
def _update_device(self, msg='', wait=True):
|
def _update_device(self, msg='', wait=True):
|
||||||
'''
|
'''
|
||||||
Trigger a sync, wait for completion
|
Trigger a sync, wait for completion
|
||||||
|
@ -256,7 +256,7 @@ class MetaInformation(object):
|
|||||||
setattr(self, x, getattr(mi, x, None))
|
setattr(self, x, getattr(mi, x, None))
|
||||||
|
|
||||||
def print_all_attributes(self):
|
def print_all_attributes(self):
|
||||||
for x in ('author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
|
for x in ('title','author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
|
||||||
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
|
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
|
||||||
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
||||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
||||||
|
@ -918,7 +918,7 @@ class OPF(object):
|
|||||||
for attr in ('title', 'authors', 'author_sort', 'title_sort',
|
for attr in ('title', 'authors', 'author_sort', 'title_sort',
|
||||||
'publisher', 'series', 'series_index', 'rating',
|
'publisher', 'series', 'series_index', 'rating',
|
||||||
'isbn', 'language', 'tags', 'category', 'comments',
|
'isbn', 'language', 'tags', 'category', 'comments',
|
||||||
'pubdate'):
|
'pubdate','timestamp'):
|
||||||
val = getattr(mi, attr, None)
|
val = getattr(mi, attr, None)
|
||||||
if val is not None and val != [] and val != (None, None):
|
if val is not None and val != [] and val != (None, None):
|
||||||
setattr(self, attr, val)
|
setattr(self, attr, val)
|
||||||
|
@ -1140,12 +1140,6 @@ class BasicNewsRecipe(Recipe):
|
|||||||
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
|
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
|
||||||
mi.publisher = __appname__
|
mi.publisher = __appname__
|
||||||
mi.author_sort = __appname__
|
mi.author_sort = __appname__
|
||||||
if self.output_profile.name == 'iPad':
|
|
||||||
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
|
||||||
mi = MetaInformation(self.short_title(), [date_as_author])
|
|
||||||
mi.publisher = __appname__
|
|
||||||
sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
|
|
||||||
mi.author_sort = '%s %s' % (sort_author, strftime('%Y-%m-%d'))
|
|
||||||
mi.publication_type = 'periodical:'+self.publication_type
|
mi.publication_type = 'periodical:'+self.publication_type
|
||||||
mi.timestamp = nowf()
|
mi.timestamp = nowf()
|
||||||
mi.comments = self.description
|
mi.comments = self.description
|
||||||
|
Loading…
x
Reference in New Issue
Block a user