mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Internationalization fixes in apple driver
This commit is contained in:
parent
3a7d5c431f
commit
462b2e6c44
@ -31,6 +31,37 @@ if isosx:
|
|||||||
|
|
||||||
if iswindows:
|
if iswindows:
|
||||||
import pythoncom, win32com.client
|
import pythoncom, win32com.client
|
||||||
|
|
||||||
|
class ITUNES(DevicePlugin):
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
pythoncom.CoInitialize()
|
||||||
|
finally:
|
||||||
|
pythoncom.CoUninitialize()
|
||||||
|
'''
|
||||||
|
|
||||||
|
name = 'Apple device interface'
|
||||||
|
gui_name = 'Apple device'
|
||||||
|
icon = I('devices/ipad.png')
|
||||||
|
description = _('Communicate with iBooks through iTunes.')
|
||||||
|
supported_platforms = ['osx','windows']
|
||||||
|
author = 'GRiker'
|
||||||
|
#: The version of this plugin as a 3-tuple (major, minor, revision)
|
||||||
|
version = (0, 4, 0)
|
||||||
|
|
||||||
|
OPEN_FEEDBACK_MESSAGE = _(
|
||||||
|
'Apple device detected, launching iTunes, please wait ...')
|
||||||
|
|
||||||
|
FORMATS = ['epub']
|
||||||
|
|
||||||
|
# Product IDs:
|
||||||
|
# 0x1292:iPhone 3G
|
||||||
|
# 0x129a:iPad
|
||||||
|
VENDOR_ID = [0x05ac]
|
||||||
|
PRODUCT_ID = [0x129a]
|
||||||
|
BCD = [0x01]
|
||||||
|
|
||||||
|
# iTunes enumerations
|
||||||
Sources = [
|
Sources = [
|
||||||
'Unknown',
|
'Unknown',
|
||||||
'Library',
|
'Library',
|
||||||
@ -48,34 +79,27 @@ if iswindows:
|
|||||||
'BMP'
|
'BMP'
|
||||||
]
|
]
|
||||||
|
|
||||||
class ITUNES(DevicePlugin):
|
PlaylistKind = [
|
||||||
'''
|
'Unknown',
|
||||||
try:
|
'Library',
|
||||||
pythoncom.CoInitialize()
|
'User',
|
||||||
finally:
|
'CD',
|
||||||
pythoncom.CoUninitialize()
|
'Device',
|
||||||
'''
|
'Radio Tuner'
|
||||||
|
]
|
||||||
|
|
||||||
name = 'Apple device interface'
|
PlaylistSpecialKind = [
|
||||||
gui_name = 'Apple device'
|
'Unknown',
|
||||||
icon = I('devices/ipad.png')
|
'Purchased Music',
|
||||||
description = _('Communicate with iBooks through iTunes.')
|
'Party Shuffle',
|
||||||
supported_platforms = ['osx','windows']
|
'Podcasts',
|
||||||
author = 'GRiker'
|
'Folder',
|
||||||
#: The version of this plugin as a 3-tuple (major, minor, revision)
|
'Video',
|
||||||
version = (1, 0, 0)
|
'Music',
|
||||||
|
'Movies',
|
||||||
OPEN_FEEDBACK_MESSAGE = _(
|
'TV Shows',
|
||||||
'Apple device detected, launching iTunes, please wait ...')
|
'Books',
|
||||||
|
]
|
||||||
FORMATS = ['epub']
|
|
||||||
|
|
||||||
# Product IDs:
|
|
||||||
# 0x1292:iPhone 3G
|
|
||||||
# 0x129a:iPad
|
|
||||||
VENDOR_ID = [0x05ac]
|
|
||||||
PRODUCT_ID = [0x129a]
|
|
||||||
BCD = [0x01]
|
|
||||||
|
|
||||||
# Properties
|
# Properties
|
||||||
cached_books = {}
|
cached_books = {}
|
||||||
@ -459,12 +483,14 @@ class ITUNES(DevicePlugin):
|
|||||||
if isosx:
|
if isosx:
|
||||||
self.iTunes.eject(self.sources['iPod'])
|
self.iTunes.eject(self.sources['iPod'])
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
try:
|
if 'iPod' in self.sources:
|
||||||
pythoncom.CoInitialize()
|
try:
|
||||||
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
pythoncom.CoInitialize()
|
||||||
self.iTunes.sources.ItemByName(self.sources['iPod']).EjectIPod()
|
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
||||||
finally:
|
self.iTunes.sources.ItemByName(self.sources['iPod']).EjectIPod()
|
||||||
pythoncom.CoUninitialize()
|
|
||||||
|
finally:
|
||||||
|
pythoncom.CoUninitialize()
|
||||||
|
|
||||||
self.iTunes = None
|
self.iTunes = None
|
||||||
self.sources = None
|
self.sources = None
|
||||||
@ -635,7 +661,7 @@ class ITUNES(DevicePlugin):
|
|||||||
if self.update_needed:
|
if self.update_needed:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(' calling _update_device')
|
self.log.info(' calling _update_device')
|
||||||
self._update_device(msg=self.update_msg)
|
self._update_device(msg=self.update_msg, wait=False)
|
||||||
self.update_needed = False
|
self.update_needed = False
|
||||||
|
|
||||||
# Get actual size of updated books on device
|
# Get actual size of updated books on device
|
||||||
@ -736,12 +762,13 @@ class ITUNES(DevicePlugin):
|
|||||||
self.problem_msg = _("Some cover art could not be converted.\n"
|
self.problem_msg = _("Some cover art could not be converted.\n"
|
||||||
"Click 'Show Details' for a list.")
|
"Click 'Show Details' for a list.")
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info("ITUNES.upload_books():")
|
||||||
|
self._dump_files(files, header='upload_books()')
|
||||||
|
self._dump_cached_books('upload_books()')
|
||||||
|
self._dump_update_list('upload_books()')
|
||||||
|
|
||||||
if isosx:
|
if isosx:
|
||||||
if DEBUG:
|
|
||||||
self.log.info("ITUNES.upload_books():")
|
|
||||||
self._dump_files(files, header='upload_books()')
|
|
||||||
self._dump_cached_books('upload_books()')
|
|
||||||
self._dump_update_list('upload_books()')
|
|
||||||
for (i,file) in enumerate(files):
|
for (i,file) in enumerate(files):
|
||||||
path = self.path_template % (metadata[i].title, metadata[i].author[0])
|
path = self.path_template % (metadata[i].title, metadata[i].author[0])
|
||||||
# Delete existing from Library|Books, add to self.update_list
|
# Delete existing from Library|Books, add to self.update_list
|
||||||
@ -836,11 +863,36 @@ class ITUNES(DevicePlugin):
|
|||||||
try:
|
try:
|
||||||
pythoncom.CoInitialize()
|
pythoncom.CoInitialize()
|
||||||
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
||||||
lib = self.iTunes.sources.ItemByName('Library')
|
|
||||||
lib_playlists = [pl.Name for pl in lib.Playlists]
|
for source in self.iTunes.sources:
|
||||||
if not 'Books' in lib_playlists:
|
if source.Kind == self.Sources.index('Library'):
|
||||||
self.log.error(" no 'Books' playlist in Library")
|
lib = source
|
||||||
library_books = lib.Playlists.ItemByName('Books')
|
if DEBUG:
|
||||||
|
self.log.info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind]))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Library source not found")
|
||||||
|
|
||||||
|
if lib is not None:
|
||||||
|
lib_books = None
|
||||||
|
for pl in lib.Playlists:
|
||||||
|
if self.PlaylistKind[pl.Kind] == 'User' and self.PlaylistSpecialKind[pl.SpecialKind] == 'Books':
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Books playlist: '%s' special_kind: '%s'" % (pl.Name, self.PlaylistSpecialKind[pl.SpecialKind]))
|
||||||
|
lib_books = pl
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.error(" no Books playlist found")
|
||||||
|
|
||||||
|
#
|
||||||
|
# lib = self.iTunes.sources.ItemByName('Library')
|
||||||
|
# lib_playlists = [pl.Name for pl in lib.Playlists]
|
||||||
|
# if not 'Books' in lib_playlists:
|
||||||
|
# self.log.error(" no 'Books' playlist in Library")
|
||||||
|
# library_books = lib.Playlists.ItemByName('Books')
|
||||||
|
#
|
||||||
|
|
||||||
for (i,file) in enumerate(files):
|
for (i,file) in enumerate(files):
|
||||||
path = self.path_template % (metadata[i].title, metadata[i].author[0])
|
path = self.path_template % (metadata[i].title, metadata[i].author[0])
|
||||||
@ -859,10 +911,10 @@ class ITUNES(DevicePlugin):
|
|||||||
|
|
||||||
# Add to iTunes Library|Books
|
# Add to iTunes Library|Books
|
||||||
if isinstance(file,PersistentTemporaryFile):
|
if isinstance(file,PersistentTemporaryFile):
|
||||||
op_status = library_books.AddFile(file._name)
|
op_status = lib_books.AddFile(file._name)
|
||||||
self.log.info("ITUNES.upload_books():\n iTunes adding '%s'" % file._name)
|
self.log.info("ITUNES.upload_books():\n iTunes adding '%s'" % file._name)
|
||||||
else:
|
else:
|
||||||
op_status = library_books.AddFile(file)
|
op_status = lib_books.AddFile(file)
|
||||||
self.log.info(" iTunes adding '%s'" % file)
|
self.log.info(" iTunes adding '%s'" % file)
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -1060,20 +1112,6 @@ class ITUNES(DevicePlugin):
|
|||||||
ub['author']))
|
ub['author']))
|
||||||
self.log.info()
|
self.log.info()
|
||||||
|
|
||||||
def _find_device_book(self, cached_book):
|
|
||||||
'''
|
|
||||||
Windows-only method to get a handle to a device book in the current pythoncom session
|
|
||||||
'''
|
|
||||||
SearchField = ['All','Visible','Artists','Titles','Composers','SongNames']
|
|
||||||
if iswindows:
|
|
||||||
dev_books = self.iTunes.sources.ItemByName(self.sources['iPod']).Playlists.ItemByName('Books')
|
|
||||||
hits = dev_books.Search(cached_book['title'],SearchField.index('Titles'))
|
|
||||||
if hits:
|
|
||||||
for hit in hits:
|
|
||||||
if hit.Artist == cached_book['author']:
|
|
||||||
return hit
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _find_library_book(self, cached_book):
|
def _find_library_book(self, cached_book):
|
||||||
'''
|
'''
|
||||||
Windows-only method to get a handle to a library book in the current pythoncom session
|
Windows-only method to get a handle to a library book in the current pythoncom session
|
||||||
@ -1083,7 +1121,28 @@ class ITUNES(DevicePlugin):
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info("ITUNES._find_library_book()")
|
self.log.info("ITUNES._find_library_book()")
|
||||||
self.log.info(" looking for '%s' by %s" % (cached_book['title'], cached_book['author']))
|
self.log.info(" looking for '%s' by %s" % (cached_book['title'], cached_book['author']))
|
||||||
lib_books = self.iTunes.sources.ItemByName('Library').Playlists.ItemByName('Books')
|
|
||||||
|
for source in self.iTunes.sources:
|
||||||
|
if source.Kind == self.Sources.index('Library'):
|
||||||
|
lib = source
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind]))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Library source not found")
|
||||||
|
|
||||||
|
if lib is not None:
|
||||||
|
lib_books = None
|
||||||
|
for pl in lib.Playlists:
|
||||||
|
if self.PlaylistKind[pl.Kind] == 'User' and self.PlaylistSpecialKind[pl.SpecialKind] == 'Books':
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Books playlist: '%s' special_kind: '%s'" % (pl.Name, self.PlaylistSpecialKind[pl.SpecialKind]))
|
||||||
|
lib_books = pl
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.error(" no Books playlist found")
|
||||||
|
|
||||||
attempts = 9
|
attempts = 9
|
||||||
while attempts:
|
while attempts:
|
||||||
@ -1121,12 +1180,6 @@ class ITUNES(DevicePlugin):
|
|||||||
except:
|
except:
|
||||||
zfw = zipfile.ZipFile(archive_path, mode='a')
|
zfw = zipfile.ZipFile(archive_path, mode='a')
|
||||||
else:
|
else:
|
||||||
# if DEBUG:
|
|
||||||
# if isosx:
|
|
||||||
# self.log.info("ITUNES._generate_thumbnail(): cached thumb found for '%s'" % book.name())
|
|
||||||
# elif iswindows:
|
|
||||||
# self.log.info("ITUNES._generate_thumbnail(): cached thumb found for '%s'" % book.Name)
|
|
||||||
|
|
||||||
return thumb_data
|
return thumb_data
|
||||||
|
|
||||||
if isosx:
|
if isosx:
|
||||||
@ -1160,7 +1213,7 @@ class ITUNES(DevicePlugin):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Save the cover from iTunes
|
# Save the cover from iTunes
|
||||||
tmp_thumb = os.path.join(tempfile.gettempdir(), "thumb.%s" % ArtworkFormat[book.Artwork.Item(1).Format])
|
tmp_thumb = os.path.join(tempfile.gettempdir(), "thumb.%s" % self.ArtworkFormat[book.Artwork.Item(1).Format])
|
||||||
book.Artwork.Item(1).SaveArtworkToFile(tmp_thumb)
|
book.Artwork.Item(1).SaveArtworkToFile(tmp_thumb)
|
||||||
try:
|
try:
|
||||||
# Resize the cover
|
# Resize the cover
|
||||||
@ -1184,8 +1237,6 @@ class ITUNES(DevicePlugin):
|
|||||||
def _get_device_book_size(self, title, author):
|
def _get_device_book_size(self, title, author):
|
||||||
'''
|
'''
|
||||||
Fetch the size of a book stored on the device
|
Fetch the size of a book stored on the device
|
||||||
|
|
||||||
Windows: If sync-in-progress, this call blocked until sync completes
|
|
||||||
'''
|
'''
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info("ITUNES._get_device_book_size():\n looking for title: '%s' author: '%s'" %
|
self.log.info("ITUNES._get_device_book_size():\n looking for title: '%s' author: '%s'" %
|
||||||
@ -1214,53 +1265,134 @@ class ITUNES(DevicePlugin):
|
|||||||
|
|
||||||
def _get_device_books(self):
|
def _get_device_books(self):
|
||||||
'''
|
'''
|
||||||
Assumes pythoncom wrapper
|
Assumes pythoncom wrapper for Windows
|
||||||
'''
|
'''
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info("\nITUNES._get_device_books()")
|
||||||
|
|
||||||
|
device_books = []
|
||||||
if isosx:
|
if isosx:
|
||||||
if 'iPod' in self.sources:
|
if 'iPod' in self.sources:
|
||||||
connected_device = self.sources['iPod']
|
connected_device = self.sources['iPod']
|
||||||
if 'Books' in self.iTunes.sources[connected_device].playlists.name():
|
device = self.iTunes.sources[connected_device]
|
||||||
return self.iTunes.sources[connected_device].playlists['Books'].file_tracks()
|
for pl in device.playlists():
|
||||||
return []
|
if pl.special_kind() == appscript.k.Books:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Book playlist: '%s' special_kind: '%s'" % (pl.name(), pl.special_kind()))
|
||||||
|
books = pl.file_tracks()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.log.error(" book_playlist not found")
|
||||||
|
|
||||||
|
for book in books:
|
||||||
|
if book.kind() in ['Book','Protected book']:
|
||||||
|
device_books.append(book)
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" ignoring '%s' of type '%s'" % (book.name(), book.kind()))
|
||||||
|
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
if 'iPod' in self.sources:
|
if 'iPod' in self.sources:
|
||||||
connected_device = self.sources['iPod']
|
try:
|
||||||
dev = self.iTunes.sources.ItemByName(connected_device)
|
pythoncom.CoInitialize()
|
||||||
dev_playlists = [pl.Name for pl in dev.Playlists]
|
connected_device = self.sources['iPod']
|
||||||
if 'Books' in dev_playlists:
|
device = self.iTunes.sources.ItemByName(connected_device)
|
||||||
return self.iTunes.sources.ItemByName(connected_device).Playlists.ItemByName('Books').Tracks
|
|
||||||
else:
|
dev_books = None
|
||||||
return []
|
for pl in device.Playlists:
|
||||||
if DEBUG:
|
if self.PlaylistKind[pl.Kind] == 'User' and self.PlaylistSpecialKind[pl.SpecialKind] == 'Books':
|
||||||
self.log.warning('ITUNES._get_device_book(): No iPod device connected')
|
if DEBUG:
|
||||||
return []
|
self.log.info(" Books playlist: '%s' special_kind: '%s'" % (pl.Name, self.PlaylistSpecialKind[pl.SpecialKind]))
|
||||||
|
dev_books = pl.Tracks
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" no Books playlist found")
|
||||||
|
|
||||||
|
for book in dev_books:
|
||||||
|
if book.KindAsString in ['Book','Protected book']:
|
||||||
|
device_books.append(book)
|
||||||
|
else:
|
||||||
|
self.log.info(" ignoring '%s' of type %s" % (book.Name, book.KindAsString))
|
||||||
|
|
||||||
|
finally:
|
||||||
|
pythoncom.CoUninitialize()
|
||||||
|
|
||||||
|
return device_books
|
||||||
|
|
||||||
def _get_library_books(self):
|
def _get_library_books(self):
|
||||||
'''
|
'''
|
||||||
Populate a dict of paths from iTunes Library|Books
|
Populate a dict of paths from iTunes Library|Books
|
||||||
'''
|
'''
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info("\nITUNES._get_library_books()")
|
||||||
|
|
||||||
library_books = {}
|
library_books = {}
|
||||||
|
lib = None
|
||||||
|
|
||||||
if isosx:
|
if isosx:
|
||||||
lib = self.iTunes.sources['library']
|
for source in self.iTunes.sources():
|
||||||
if 'Books' in lib.playlists.name():
|
if source.kind() == appscript.k.library:
|
||||||
lib_books = lib.playlists['Books'].file_tracks()
|
lib = source
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Library source: '%s' kind: %s" % (lib.name(), lib.kind()))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.error(' Library source not found')
|
||||||
|
|
||||||
|
if lib is not None:
|
||||||
|
lib_books = None
|
||||||
|
for pl in lib.playlists():
|
||||||
|
if pl.special_kind() == appscript.k.Books:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Books playlist: '%s' special_kind: '%s'" % (pl.name(), pl.special_kind()))
|
||||||
|
break
|
||||||
|
lib_books = pl.file_tracks()
|
||||||
for book in lib_books:
|
for book in lib_books:
|
||||||
path = self.path_template % (book.name(), book.artist())
|
if book.kind() in ['Book','Protected book']:
|
||||||
library_books[path] = book
|
path = self.path_template % (book.name(), book.artist())
|
||||||
|
library_books[path] = book
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" ignoring library book of type '%s'" % book.kind())
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info('ITUNES._get_library_books():\n No Books playlist')
|
||||||
|
|
||||||
|
|
||||||
elif iswindows:
|
elif iswindows:
|
||||||
|
lib = None
|
||||||
try:
|
try:
|
||||||
pythoncom.CoInitialize()
|
pythoncom.CoInitialize()
|
||||||
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
||||||
lib = self.iTunes.sources.ItemByName('Library')
|
for source in self.iTunes.sources:
|
||||||
lib_playlists = [pl.Name for pl in lib.Playlists]
|
if source.Kind == self.Sources.index('Library'):
|
||||||
if 'Books' in lib_playlists:
|
lib = source
|
||||||
lib_books = lib.Playlists.ItemByName('Books').Tracks
|
self.log.info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind]))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.log.error(" Library source not found")
|
||||||
|
|
||||||
|
if lib is not None:
|
||||||
|
lib_books = None
|
||||||
|
for pl in lib.Playlists:
|
||||||
|
if self.PlaylistKind[pl.Kind] == 'User' and self.PlaylistSpecialKind[pl.SpecialKind] == 'Books':
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" Books playlist: '%s' special_kind: '%s'" % (pl.Name, self.PlaylistSpecialKind[pl.SpecialKind]))
|
||||||
|
lib_books = pl.Tracks
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.error(" no Books playlist found")
|
||||||
|
|
||||||
for book in lib_books:
|
for book in lib_books:
|
||||||
path = self.path_template % (book.Name, book.Artist)
|
if book.KindAsString in ['Book','Protected book']:
|
||||||
library_books[path] = book
|
path = self.path_template % (book.Name, book.Artist)
|
||||||
|
library_books[path] = book
|
||||||
|
else:
|
||||||
|
if DEBUG:
|
||||||
|
self.log.info(" ignoring '%s' of type %s" % (book.Name, book.KindAsString))
|
||||||
finally:
|
finally:
|
||||||
pythoncom.CoUninitialize()
|
pythoncom.CoUninitialize()
|
||||||
|
|
||||||
@ -1455,11 +1587,9 @@ class ITUNES(DevicePlugin):
|
|||||||
sys.stdout.write('\n')
|
sys.stdout.write('\n')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
break
|
break
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
pythoncom.CoUninitialize()
|
pythoncom.CoUninitialize()
|
||||||
|
|
||||||
|
|
||||||
class BookList(list):
|
class BookList(list):
|
||||||
'''
|
'''
|
||||||
A list of books. Each Book object must have the fields:
|
A list of books. Each Book object must have the fields:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user