package fr.free.nrw.commons; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.Entity; import androidx.room.PrimaryKey; import org.apache.commons.lang3.StringUtils; import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.gallery.ExtMetadata; import org.wikipedia.gallery.ImageInfo; import org.wikipedia.page.PageTitle; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.utils.MediaDataExtractorUtil; @Entity public class Media implements Parcelable { public static final Media EMPTY = new Media(""); // Primary metadata fields @Nullable public Uri localUri; public String thumbUrl; public String imageUrl; public String filename; public String description; // monolingual description on input... public String discussion; long dataLength; public Date dateCreated; @Nullable public Date dateUploaded; public int width; public int height; public String license; public String licenseUrl; public String creator; public ArrayList categories; // as loaded at runtime? public boolean requestedDeletion; public HashMap descriptions; // multilingual descriptions as loaded public HashMap tags = new HashMap<>(); @Nullable public LatLng coordinates; /** * Provides local constructor */ protected Media() { this.categories = new ArrayList<>(); this.descriptions = new HashMap<>(); } /** * Provides a minimal constructor * * @param filename Media filename */ public Media(String filename) { this(); this.filename = filename; } /** * Provide Media constructor * @param localUri Media URI * @param imageUrl Media image URL * @param filename Media filename * @param description Media description * @param dataLength Media date length * @param dateCreated Media creation date * @param dateUploaded Media date uploaded * @param creator Media creator */ public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) { this(); this.localUri = localUri; this.thumbUrl = imageUrl; this.imageUrl = imageUrl; this.filename = filename; this.description = description; this.dataLength = dataLength; this.dateCreated = dateCreated; this.dateUploaded = dateUploaded; this.creator = creator; this.categories = new ArrayList<>(); this.descriptions = new HashMap<>(); } /** * Creating Media object from MWQueryPage. * Earlier only basic details were set for the media object but going forward, * a full media object(with categories, descriptions, coordinates etc) can be constructed using this method * * @param page response from the API * @return Media object */ @Nullable public static Media from(MwQueryPage page) { ImageInfo imageInfo = page.imageInfo(); if (imageInfo == null) { return null; } ExtMetadata metadata = imageInfo.getMetadata(); if (metadata == null) { Media media = new Media(null, imageInfo.getOriginalUrl(), page.title(), "", 0, null, null, null); if (!StringUtils.isBlank(imageInfo.getThumbUrl())) { media.setThumbUrl(imageInfo.getThumbUrl()); } return media; } Media media = new Media(null, imageInfo.getOriginalUrl(), page.title(), "", 0, safeParseDate(metadata.dateTime()), safeParseDate(metadata.dateTime()), getArtist(metadata) ); if (!StringUtils.isBlank(imageInfo.getThumbUrl())) { media.setThumbUrl(imageInfo.getThumbUrl()); } String language = Locale.getDefault().getLanguage(); if (StringUtils.isBlank(language)) { language = "default"; } media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription())); media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories())); String latitude = metadata.getGpsLatitude(); String longitude = metadata.getGpsLongitude(); if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) { LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0); media.setCoordinates(latLng); } media.setLicenseInformation(metadata.licenseShortName(), metadata.licenseUrl()); return media; } /** * This method extracts the Commons Username from the artist HTML information * @param metadata * @return */ private static String getArtist(ExtMetadata metadata) { try { String artistHtml = metadata.artist(); return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">")) .replace("title=\"User:", ""); } catch (Exception ex) { return ""; } } public String getThumbUrl() { return thumbUrl; } /** * Gets tag of media * @param key Media key * @return Media tag */ public Object getTag(String key) { return tags.get(key); } /** * Modifies( or creates a) tag of media * @param key Media key * @param value Media value */ public void setTag(String key, String value) { tags.put(key, value); } /** * Gets media display title * @return Media title */ @NonNull public String getDisplayTitle() { return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : ""; } /** * Gets file page title * @return New media page title */ @NonNull public PageTitle getPageTitle() { return Utils.getPageTitle(getFilename()); } /** * Gets local URI * @return Media local URI */ public Uri getLocalUri() { return localUri; } /** * Gets image URL * can be null. * @return Image URL */ @Nullable public String getImageUrl() { return imageUrl; } /** * Gets the name of the file. * @return file name as a string */ public String getFilename() { return filename; } /** * Sets the name of the file. * @param filename the new name of the file */ public void setFilename(String filename) { this.filename = filename; } /** * Sets the discussion of the file. * @param discussion */ public void setDiscussion(String discussion) { this.discussion = discussion; } /** * Gets the file discussion as a string. * @return file discussion as a string */ public String getDiscussion() { return discussion; } /** * Gets the file description. * @return file description as a string */ public String getDescription() { return description; } /** * Sets the file description. * @param description the new description of the file */ public void setDescription(String description) { this.description = description; } /** * Gets the dataLength of the file. * @return file dataLength as a long */ public long getDataLength() { return dataLength; } /** * Sets the dataLength of the file. * @param dataLength as a long */ public void setDataLength(long dataLength) { this.dataLength = dataLength; } /** * Gets the creation date of the file. * @return creation date as a Date */ public Date getDateCreated() { return dateCreated; } /** * Sets the creation date of the file. * @param date creation date as a Date */ public void setDateCreated(Date date) { this.dateCreated = date; } /** * Gets the upload date of the file. * Can be null. * @return upload date as a Date */ public @Nullable Date getDateUploaded() { return dateUploaded; } /** * Gets the name of the creator of the file. * @return creator name as a String */ public String getCreator() { return creator; } /** * Sets the creator name of the file. * @param creator creator name as a string */ public void setCreator(String creator) { this.creator = creator; } /** * Gets the width of the media. * @return file width as an int */ public int getWidth() { return width; } /** * Sets the width of the media. * @param width file width as an int */ public void setWidth(int width) { this.width = width; } /** * Gets the height of the media. * @return file height as an int */ public int getHeight() { return height; } /** * Sets the height of the media. * @param height file height as an int */ public void setHeight(int height) { this.height = height; } /** * Gets the license name of the file. * @return license as a String */ public String getLicense() { return license; } public void setThumbUrl(String thumbUrl) { this.thumbUrl = thumbUrl; } public String getLicenseUrl() { return licenseUrl; } /** * Sets the license name of the file. * @param license license name as a String */ public void setLicenseInformation(String license, String licenseUrl) { this.license = license; if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) { licenseUrl = "https://" + licenseUrl; } this.licenseUrl = licenseUrl; } /** * Gets the coordinates of where the file was created. * @return file coordinates as a LatLng */ public @Nullable LatLng getCoordinates() { return coordinates; } /** * Sets the coordinates of where the file was created. * @param coordinates file coordinates as a LatLng */ public void setCoordinates(@Nullable LatLng coordinates) { this.coordinates = coordinates; } /** * Gets the categories the file falls under. * @return file categories as an ArrayList of Strings */ @SuppressWarnings("unchecked") public ArrayList getCategories() { return (ArrayList) categories.clone(); // feels dirty } /** * Sets the categories the file falls under. *

* Does not append: i.e. will clear the current categories * and then add the specified ones. * @param categories file categories as a list of Strings */ public void setCategories(List categories) { this.categories.clear(); this.categories.addAll(categories); } /** * Modifies (or sets) media descriptions * @param descriptions Media descriptions */ void setDescriptions(Map descriptions) { this.descriptions.clear(); this.descriptions.putAll(descriptions); } /** * Gets media description in preferred language * @param preferredLanguage Language preferred * @return Description in preferred language */ public String getDescription(String preferredLanguage) { if (descriptions.containsKey(preferredLanguage)) { // See if the requested language is there. return descriptions.get(preferredLanguage); } else if (descriptions.containsKey("en")) { // Ah, English. Language of the world, until the Chinese crush us. return descriptions.get("en"); } else if (descriptions.containsKey("default")) { // No languages marked... return descriptions.get("default"); } else { // FIXME: return the first available non-English description? return ""; } } @Nullable private static Date safeParseDate(String dateStr) { try { return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr); } catch (ParseException e) { return null; } } /** * Set requested deletion to true */ public void setRequestedDeletion(){ requestedDeletion = true; } /** * Get the value of requested deletion * @return boolean requestedDeletion */ public boolean getRequestedDeletion(){ return requestedDeletion; } /** * Sets the license name of the file. * * @param license license name as a String */ public void setLicense(String license) { this.license = license; } @Override public int describeContents() { return 0; } /** * Creates a way to transfer information between two or more * activities. * @param dest Instance of Parcel * @param flags Parcel flag */ @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(this.localUri, flags); dest.writeString(this.thumbUrl); dest.writeString(this.imageUrl); dest.writeString(this.filename); dest.writeString(this.description); dest.writeString(this.discussion); dest.writeLong(this.dataLength); dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1); dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1); dest.writeInt(this.width); dest.writeInt(this.height); dest.writeString(this.license); dest.writeString(this.licenseUrl); dest.writeString(this.creator); dest.writeStringList(this.categories); dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0); dest.writeSerializable(this.descriptions); dest.writeSerializable(this.tags); dest.writeParcelable(this.coordinates, flags); } protected Media(Parcel in) { this.localUri = in.readParcelable(Uri.class.getClassLoader()); this.thumbUrl = in.readString(); this.imageUrl = in.readString(); this.filename = in.readString(); this.description = in.readString(); this.discussion = in.readString(); this.dataLength = in.readLong(); long tmpDateCreated = in.readLong(); this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated); long tmpDateUploaded = in.readLong(); this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded); this.width = in.readInt(); this.height = in.readInt(); this.license = in.readString(); this.licenseUrl = in.readString(); this.creator = in.readString(); this.categories = in.createStringArrayList(); this.requestedDeletion = in.readByte() != 0; this.descriptions = (HashMap) in.readSerializable(); this.tags = (HashMap) in.readSerializable(); this.coordinates = in.readParcelable(LatLng.class.getClassLoader()); } public static final Creator CREATOR = new Creator() { @Override public Media createFromParcel(Parcel source) { return new Media(source); } @Override public Media[] newArray(int size) { return new Media[size]; } }; }