added intrash support for issue #179
This commit is contained in:
@@ -875,7 +875,7 @@ for row in results:
|
||||
conn.close()
|
||||
```
|
||||
|
||||
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None)`
|
||||
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None, intrash=False)`
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@@ -896,7 +896,8 @@ photos = photosdb.photos(
|
||||
images = bool,
|
||||
movies = bool,
|
||||
from_date = datetime.datetime,
|
||||
to_date = datetime.datetime
|
||||
to_date = datetime.datetime,
|
||||
intrash = bool,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -908,6 +909,7 @@ photos = photosdb.photos(
|
||||
- ```movies```: bool; if True, returns movies/videos; default is False
|
||||
- ```from_date```: datetime.datetime; if provided, finds photos where creation date >= from_date; default is None
|
||||
- ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
|
||||
- ```intrash```: if True, finds only photos in the "Recently Deleted" or trash folder, if False does not find any photos in the trash; default is False
|
||||
|
||||
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
|
||||
|
||||
@@ -1047,6 +1049,9 @@ Returns `True` if the picture has been marked as a favorite, otherwise `False`
|
||||
#### `hidden`
|
||||
Returns `True` if the picture has been marked as hidden, otherwise `False`
|
||||
|
||||
#### `intrash`
|
||||
Returns `True` if the picture is in the trash ('Recently Deleted' folder), otherwise `False`
|
||||
|
||||
#### `location`
|
||||
Returns latitude and longitude as a tuple of floats (latitude, longitude). If location is not set, latitude and longitude are returned as `None`
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.29.30"
|
||||
__version__ = "0.30.0"
|
||||
|
||||
@@ -417,6 +417,11 @@ class PhotoInfo:
|
||||
""" True if picture is hidden """
|
||||
return True if self._info["hidden"] == 1 else False
|
||||
|
||||
@property
|
||||
def intrash(self):
|
||||
""" True if picture is in trash ('Recently Deleted' folder)"""
|
||||
return self._info["intrash"]
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" returns (latitude, longitude) as float in degrees or None """
|
||||
|
||||
@@ -715,9 +715,10 @@ class PhotosDB:
|
||||
RKVersion.specialType, RKMaster.modelID, null, RKVersion.momentUuid,
|
||||
RKVersion.rawMasterUuid,
|
||||
RKVersion.nonRawMasterUuid,
|
||||
RKMaster.alternateMasterUuid
|
||||
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
||||
RKVersion.masterUuid = RKMaster.uuid"""
|
||||
RKMaster.alternateMasterUuid,
|
||||
RKVersion.isInTrash
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
else:
|
||||
c.execute(
|
||||
@@ -734,9 +735,10 @@ class PhotosDB:
|
||||
RKVersion.momentUuid,
|
||||
RKVersion.rawMasterUuid,
|
||||
RKVersion.nonRawMasterUuid,
|
||||
RKMaster.alternateMasterUuid
|
||||
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
||||
RKVersion.masterUuid = RKMaster.uuid"""
|
||||
RKMaster.alternateMasterUuid,
|
||||
RKVersion.isInTrash
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
|
||||
# order of results
|
||||
@@ -772,6 +774,7 @@ class PhotosDB:
|
||||
# 29 RKVersion.rawMasterUuid, -- UUID of RAW master
|
||||
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
|
||||
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
|
||||
# 32 RKVersion.isInTrash
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -914,6 +917,9 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["non_raw_master_uuid"] = row[30]
|
||||
self._dbphotos[uuid]["alt_master_uuid"] = row[31]
|
||||
|
||||
# recently deleted items
|
||||
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
||||
|
||||
# get additional details from RKMaster, needed for RAW processing
|
||||
c.execute(
|
||||
""" SELECT
|
||||
@@ -964,8 +970,7 @@ class PhotosDB:
|
||||
RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,
|
||||
RKModelResource.attachedModelType, RKModelResource.resourceType
|
||||
FROM RKVersion
|
||||
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId
|
||||
WHERE RKVersion.isInTrash = 0 """
|
||||
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId """
|
||||
)
|
||||
|
||||
# Order of results:
|
||||
@@ -1008,8 +1013,7 @@ class PhotosDB:
|
||||
RKAdjustmentData.originator,
|
||||
RKAdjustmentData.format
|
||||
FROM RKVersion, RKAdjustmentData
|
||||
WHERE RKVersion.adjustmentUuid = RKAdjustmentData.uuid
|
||||
AND RKVersion.isInTrash = 0 """
|
||||
WHERE RKVersion.adjustmentUuid = RKAdjustmentData.uuid """
|
||||
)
|
||||
|
||||
for row in c:
|
||||
@@ -1031,8 +1035,6 @@ class PhotosDB:
|
||||
INNER JOIN RKMaster on RKVersion.masterUuid = RKMaster.uuid
|
||||
INNER JOIN RKModelResource on RKMaster.modelId = RKModelResource.attachedModelId
|
||||
WHERE RKModelResource.UTI = 'com.apple.quicktime-movie'
|
||||
AND RKMaster.isInTrash = 0
|
||||
AND RKVersion.isInTrash = 0
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1277,7 +1279,6 @@ class PhotosDB:
|
||||
"SELECT ZPERSON.ZFULLNAME, ZGENERICASSET.ZUUID "
|
||||
"FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET "
|
||||
"WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK "
|
||||
"AND ZGENERICASSET.ZTRASHEDSTATE = 0"
|
||||
)
|
||||
for person in c:
|
||||
if person[0] is None:
|
||||
@@ -1301,7 +1302,6 @@ class PhotosDB:
|
||||
"FROM ZGENERICASSET "
|
||||
"JOIN Z_26ASSETS ON Z_26ASSETS.Z_34ASSETS = ZGENERICASSET.Z_PK "
|
||||
"JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS "
|
||||
"WHERE ZGENERICASSET.ZTRASHEDSTATE = 0 "
|
||||
)
|
||||
for album in c:
|
||||
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
|
||||
@@ -1403,7 +1403,6 @@ class PhotosDB:
|
||||
"JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK "
|
||||
"JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK "
|
||||
"JOIN ZKEYWORD ON ZKEYWORD.Z_PK = Z_1KEYWORDS.Z_37KEYWORDS "
|
||||
"WHERE ZGENERICASSET.ZTRASHEDSTATE = 0 "
|
||||
)
|
||||
for keyword in c:
|
||||
if not keyword[1] in self._dbkeywords_uuid:
|
||||
@@ -1457,10 +1456,10 @@ class PhotosDB:
|
||||
ZGENERICASSET.ZCLOUDASSETGUID,
|
||||
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
|
||||
ZGENERICASSET.ZMOMENT,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
||||
ZGENERICASSET.ZTRASHEDSTATE
|
||||
FROM ZGENERICASSET
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||
WHERE ZGENERICASSET.ZTRASHEDSTATE = 0
|
||||
ORDER BY ZGENERICASSET.ZUUID """
|
||||
)
|
||||
# Order of results
|
||||
@@ -1493,6 +1492,7 @@ class PhotosDB:
|
||||
# 25 ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA -- reverse geolocation data
|
||||
# 26 ZGENERICASSET.ZMOMENT -- FK for ZMOMENT.Z_PK
|
||||
# 27 ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE -- 1 if associated RAW image is original else 0
|
||||
# 28 ZGENERICASSET.ZTRASHEDSTATE -- 0 if not in trash, 1 if in trash
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1641,6 +1641,9 @@ class PhotosDB:
|
||||
info["original_resource_choice"] = row[27]
|
||||
info["raw_is_original"] = True if row[27] == 1 else False
|
||||
|
||||
# recently deleted items
|
||||
info["intrash"] = True if row[28] == 1 else False
|
||||
|
||||
# associated RAW image info
|
||||
# will be filled in later
|
||||
info["has_raw"] = False
|
||||
@@ -2145,6 +2148,7 @@ class PhotosDB:
|
||||
movies=False,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
intrash=False,
|
||||
):
|
||||
"""
|
||||
Return a list of PhotoInfo objects
|
||||
@@ -2161,6 +2165,8 @@ class PhotosDB:
|
||||
movies: if True, returns movie files, if False, does not return movies; default is False
|
||||
from_date: return photos with creation date >= from_date (datetime.datetime object, default None)
|
||||
to_date: return photos with creation date <= to_date (datetime.datetime object, default None)
|
||||
intrash: if True, returns only images in "Recently deleted items" folder,
|
||||
if False returns only photos that aren't deleted; default is False
|
||||
"""
|
||||
|
||||
# implementation is a bit kludgy but it works
|
||||
@@ -2168,6 +2174,15 @@ class PhotosDB:
|
||||
# use results to build a list of PhotoInfo objects
|
||||
|
||||
photos_sets = [] # list of photo sets to perform intersection of
|
||||
if intrash:
|
||||
photos_sets.append(
|
||||
{p for p in self._dbphotos if self._dbphotos[p]["intrash"]}
|
||||
)
|
||||
else:
|
||||
photos_sets.append(
|
||||
{p for p in self._dbphotos if not self._dbphotos[p]["intrash"]}
|
||||
)
|
||||
|
||||
if not any([keywords, uuid, persons, albums, from_date, to_date]):
|
||||
# return all the photos, filtering for images and movies
|
||||
# append keys of all photos as a single set to photos_sets
|
||||
@@ -2176,8 +2191,7 @@ class PhotosDB:
|
||||
if albums:
|
||||
album_set = set()
|
||||
for album in albums:
|
||||
# TODO: can have >1 album with same name. This globs them together.
|
||||
# Need a way to select which album?
|
||||
# glob together albums with same name
|
||||
if album in self._dbalbum_titles:
|
||||
title_set = set()
|
||||
for album_id in self._dbalbum_titles[album]:
|
||||
@@ -2218,6 +2232,7 @@ class PhotosDB:
|
||||
logging.debug(f"Could not find person '{person}' in database")
|
||||
photos_sets.append(person_set)
|
||||
|
||||
# sourcery off
|
||||
if from_date or to_date:
|
||||
dsel = self._dbphotos
|
||||
if from_date:
|
||||
@@ -2235,7 +2250,6 @@ class PhotosDB:
|
||||
photoinfo = []
|
||||
if photos_sets: # found some photos
|
||||
# get the intersection of each argument/search criteria
|
||||
logging.debug(f"Got photo_sets: {photos_sets}")
|
||||
for p in set.intersection(*photos_sets):
|
||||
# filter for non-selected burst photos
|
||||
if self._dbphotos[p]["burst"] and not self._dbphotos[p]["burst_key"]:
|
||||
@@ -2264,5 +2278,7 @@ class PhotosDB:
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
""" returns number of photos in the database """
|
||||
""" Returns number of photos in the database
|
||||
Includes recently deleted photos and non-selected burst images
|
||||
"""
|
||||
return len(self._dbphotos)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
@@ -5,6 +5,6 @@
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-04-25T23:54:43Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-04-26T06:26:10Z</date>
|
||||
<date>2020-06-27T16:03:48Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,6 @@
|
||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||
<integer>1</integer>
|
||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||
<date>2020-04-25T23:54:29Z</date>
|
||||
<date>2020-06-27T16:03:43Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>606</integer>
|
||||
<integer>651</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
@@ -24,7 +24,7 @@
|
||||
<key>SnapshotCompletedDate</key>
|
||||
<date>2019-07-27T13:16:43Z</date>
|
||||
<key>SnapshotLastValidated</key>
|
||||
<date>2020-04-25T23:56:35Z</date>
|
||||
<date>2020-06-27T16:03:33Z</date>
|
||||
<key>SnapshotTables</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,9 @@ PHOTOS_DB = "tests/Test-10.15.5.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_PATH = "/Test-10.15.5.photoslibrary/database/photos.db"
|
||||
PHOTOS_LIBRARY_PATH = "/Test-10.15.5.photoslibrary"
|
||||
|
||||
PHOTOS_DB_LEN = 13
|
||||
PHOTOS_DB_LEN = 14
|
||||
PHOTOS_NOT_IN_TRASH_LEN = 13
|
||||
PHOTOS_IN_TRASH_LEN = 1
|
||||
|
||||
KEYWORDS = [
|
||||
"Kids",
|
||||
@@ -67,6 +69,8 @@ UUID_DICT = {
|
||||
"export_tif": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||
"in_album": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
||||
"date_invalid": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||
"not_intrash": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||
}
|
||||
|
||||
UUID_PUMPKIN_FARM = [
|
||||
@@ -393,7 +397,63 @@ def test_count():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos()
|
||||
assert len(photos) == PHOTOS_DB_LEN
|
||||
assert len(photos) == PHOTOS_NOT_IN_TRASH_LEN
|
||||
|
||||
|
||||
def test_photos_intrash_1():
|
||||
""" test PhotosDB.photos(intrash=True) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=True)
|
||||
assert len(photos) == PHOTOS_IN_TRASH_LEN
|
||||
|
||||
|
||||
def test_photos_intrash_2():
|
||||
""" test PhotosDB.photos(intrash=True) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=True)
|
||||
for p in photos:
|
||||
assert p.intrash
|
||||
|
||||
|
||||
def test_photos_intrash_2():
|
||||
""" test PhotosDB.photos(intrash=False) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=False)
|
||||
for p in photos:
|
||||
assert not p.intrash
|
||||
|
||||
|
||||
def test_photoinfo_intrash_1():
|
||||
""" Test PhotoInfo.intrash """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
|
||||
assert p.intrash
|
||||
|
||||
|
||||
def test_photoinfo_intrash_2():
|
||||
""" Test PhotoInfo.intrash and intrash=default"""
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]])
|
||||
assert not p
|
||||
|
||||
|
||||
def test_photoinfo_not_intrash():
|
||||
""" Test PhotoInfo.intrash """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
|
||||
assert not p.intrash
|
||||
|
||||
|
||||
def test_keyword_2():
|
||||
|
||||
@@ -42,8 +42,14 @@ UUID_DICT = {
|
||||
"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||
"not_favorite": "8SOE9s0XQVGsuq4ONohTng",
|
||||
"date_invalid": "YZFCPY24TUySvpu7owiqxA",
|
||||
"intrash": "3tljdX43R8+k6peNHVrJNQ",
|
||||
"not_intrash": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||
}
|
||||
|
||||
PHOTOS_DB_LEN = 8
|
||||
PHOTOS_NOT_IN_TRASH_LEN = 7
|
||||
PHOTOS_IN_TRASH_LEN = 1
|
||||
|
||||
|
||||
def test_init():
|
||||
import osxphotos
|
||||
@@ -65,7 +71,7 @@ def test_db_len():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
|
||||
assert len(photosdb) == 7
|
||||
assert len(photosdb) == PHOTOS_DB_LEN
|
||||
|
||||
|
||||
def test_os_version():
|
||||
@@ -309,7 +315,63 @@ def test_count():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos()
|
||||
assert len(photos) == 7
|
||||
assert len(photos) == PHOTOS_NOT_IN_TRASH_LEN
|
||||
|
||||
|
||||
def test_photos_intrash_1():
|
||||
""" test PhotosDB.photos(intrash=True) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=True)
|
||||
assert len(photos) == PHOTOS_IN_TRASH_LEN
|
||||
|
||||
|
||||
def test_photos_intrash_2():
|
||||
""" test PhotosDB.photos(intrash=True) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=True)
|
||||
for p in photos:
|
||||
assert p.intrash
|
||||
|
||||
|
||||
def test_photos_intrash_2():
|
||||
""" test PhotosDB.photos(intrash=False) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=False)
|
||||
for p in photos:
|
||||
assert not p.intrash
|
||||
|
||||
|
||||
def test_photoinfo_intrash_1():
|
||||
""" Test PhotoInfo.intrash """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
|
||||
assert p.intrash
|
||||
|
||||
|
||||
def test_photoinfo_intrash_2():
|
||||
""" Test PhotoInfo.intrash and intrash=default"""
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]])
|
||||
assert not p
|
||||
|
||||
|
||||
def test_photoinfo_not_intrash():
|
||||
""" Test PhotoInfo.intrash """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
|
||||
assert not p.intrash
|
||||
|
||||
|
||||
def test_keyword_2():
|
||||
|
||||
Reference in New Issue
Block a user