From f1c563e3bc36227e77844d2d0caf63e171df2a8d Mon Sep 17 00:00:00 2001 From: LagradOst Date: Thu, 17 Jun 2021 00:31:41 +0200 Subject: [PATCH] another provider and a bunch of bug fixes --- app/src/main/AndroidManifest.xml | 3 +- .../com/lagradost/cloudstream3/MainAPI.kt | 11 +- .../movieproviders/MeloMovieProvider.kt | 155 ++++++++++++++++++ .../cloudstream3/ui/ControllerActivity.kt | 13 +- .../cloudstream3/ui/player/PlayerFragment.kt | 22 ++- .../cloudstream3/ui/result/ResultFragment.kt | 31 +++- .../cloudstream3/ui/result/ResultViewModel.kt | 9 +- .../cloudstream3/ui/search/SearchFragment.kt | 4 +- .../cloudstream3/ui/search/SearchViewModel.kt | 5 +- .../cloudstream3/utils/CastHelper.kt | 20 ++- .../cloudstream3/utils/ExtractorApi.kt | 16 +- app/src/main/res/layout/fragment_result.xml | 1 + 12 files changed, 250 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/movieproviders/MeloMovieProvider.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e98441e9..56e5a334 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,9 +12,10 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:usesCleartextTraffic="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme" android:fullBackupContent="@xml/backup_descriptor"> + android:theme="@style/AppTheme" android:fullBackupContent="@xml/backup_descriptor" tools:targetApi="m"> diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 94386124..a5b31950 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.animeproviders.ShiroProvider +import com.lagradost.cloudstream3.movieproviders.MeloMovieProvider import com.lagradost.cloudstream3.utils.ExtractorLink import java.util.* import kotlin.collections.ArrayList @@ -20,8 +21,9 @@ object APIHolder { private const val defProvider = 0 - val apis = arrayListOf( - ShiroProvider() + val apis = arrayListOf( + ShiroProvider(), + MeloMovieProvider(), ) fun getApiFromName(apiName: String?): MainAPI { @@ -44,6 +46,7 @@ object APIHolder { abstract class MainAPI { open val name = "NONE" open val mainUrl = "NONE" + open val instantLinkLoading = false // THIS IS IF THE LINK IS STORED IN THE "DATA" open fun search(query: String): ArrayList? { // SearchResponse return null } @@ -195,12 +198,14 @@ data class MovieLoadResponse( val imdbId: Int?, ) : LoadResponse +data class TvSeriesEpisode(val name: String?, val season : Int?, val episode: Int?, val data : String) + data class TvSeriesLoadResponse( override val name: String, override val url: String, override val apiName: String, override val type: TvType, - val episodes: ArrayList, + val episodes: ArrayList, override val posterUrl: String?, override val year: Int?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/MeloMovieProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/MeloMovieProvider.kt new file mode 100644 index 00000000..1c93ed26 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/MeloMovieProvider.kt @@ -0,0 +1,155 @@ +package com.lagradost.cloudstream3.movieproviders + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class MeloMovieProvider : MainAPI() { + override val name: String + get() = "MeloMovie" + override val mainUrl: String + get() = "https://melomovie.com" + override val instantLinkLoading: Boolean + get() = true + + data class MeloMovieSearchResult( + @JsonProperty("id") val id: Int, + @JsonProperty("imdb_code") val imdbId: String, + @JsonProperty("title") val title: String, + @JsonProperty("type") val type: Int, // 1 = MOVIE, 2 = TV-SERIES + @JsonProperty("year") val year: Int?, // 1 = MOVIE, 2 = TV-SERIES + //"mppa" for tags + ) + + data class MeloMovieLink(val name: String, val link: String) + + override fun search(query: String): ArrayList? { + val url = "$mainUrl/movie/search/?name=$query" + val returnValue: ArrayList = ArrayList() + val response = khttp.get(url) + val mapped = response.let { mapper.readValue>(it.text) } + if (mapped.isEmpty()) return returnValue + + for (i in mapped) { + val currentUrl = "$mainUrl/movie/${i.id}" + val currentPoster = "$mainUrl/assets/images/poster/${i.imdbId}.jpg" + if (i.type == 2) { // TV-SERIES + returnValue.add(TvSeriesSearchResponse(i.title, + currentUrl, + currentUrl, + this.name, + TvType.TvSeries, + currentPoster, + i.year, + null)) + } else if (i.type == 1) { // MOVIE + returnValue.add(MovieSearchResponse(i.title, + currentUrl, + currentUrl, + this.name, + TvType.Movie, + currentUrl, + i.year)) + } + } + return returnValue + } + + // http not https, the links are not https! + private fun fixUrl(url: String): String { + if(url.isEmpty()) return "" + + if (url.startsWith("//")) { + return "http:$url" + } + if (!url.startsWith("http")) { + return "http://$url" + } + return url + } + + private fun serializeData(element: Element): String { + val eps = element.select("> tbody > tr") + val parsed = eps.map { + try { + val tds = it.select("> td") + val name = tds[if (tds.size == 5) 1 else 0].text() + val url = fixUrl(tds.last().selectFirst("> a").attr("data-lnk").replace(" ", "%20")) + MeloMovieLink(name, url) + } catch (e: Exception) { + MeloMovieLink("", "") + } + }.filter { it.link != "" && it.name != "" } + return mapper.writeValueAsString(parsed) + } + + override fun loadLinks(data: String, isCasting: Boolean, callback: (ExtractorLink) -> Unit): Boolean { + val links = mapper.readValue>(data) + for (link in links) { + callback.invoke(ExtractorLink(this.name, link.name, link.link, "", getQualityFromName(link.name), false)) + } + return true + } + + override fun load(slug: String): Any? { + val response = khttp.get(slug).text + + //backdrop = imgurl + fun findUsingRegex(src: String): String? { + return src.toRegex().find(response)?.groups?.get(1)?.value ?: return null + } + + val imdbId = findUsingRegex("var imdb = \"(tt[0-9]*)\"")?.toIntOrNull() + val document = Jsoup.parse(response) + val poster = document.selectFirst("img.img-fluid").attr("src") + val type = findUsingRegex("var posttype = ([0-9]*)")?.toInt() ?: return null + val titleInfo = document.selectFirst("div.movie_detail_title > div > div > h1") + val title = titleInfo.ownText() + val year = titleInfo.selectFirst("> a").text().replace("(", "").replace(")", "").toIntOrNull() + val plot = document.selectFirst("div.col-lg-12 > p").text() + + if (type == 1) { // MOVIE + val serialize = document.selectFirst("table.accordion__list") + return MovieLoadResponse(title, + slug, + this.name, + TvType.Movie, + serializeData(serialize), + poster, + year, + plot, + imdbId) + } else if (type == 2) { + val episodes = ArrayList() + val seasons = document.select("div.accordion__card") + for (s in seasons) { + val season = + s.selectFirst("> div.card-header > button > span").text().replace("Season: ", "").toIntOrNull() + val localEpisodes = s.select("> div.collapse > div > div > div.accordion__card") + for (e in localEpisodes) { + val episode = + e.selectFirst("> div.card-header > button > span").text().replace("Episode: ", "").toIntOrNull() + val links = e.selectFirst("> div.collapse > div > table.accordion__list") + val data = serializeData(links) + episodes.add(TvSeriesEpisode(null, season, episode, data)) + } + } + episodes.reverse() + return TvSeriesLoadResponse(title, + slug, + this.name, + TvType.TvSeries, + episodes, + poster, + year, + plot, + null, + imdbId) + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 80840815..824960da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -77,6 +77,7 @@ class SkipNextEpisodeController(val view: ImageView) : UIController() { data class MetadataHolder( val apiName: String, + val isMovie: Boolean, val title: String?, val poster: String?, val currentEpisodeIndex: Int, @@ -91,13 +92,13 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi init { view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setOnClickListener { - // lateinit var dialog: AlertDialog + // lateinit var dialog: AlertDialog val holder = getCurrentMetaData() if (holder != null) { val items = holder.currentLinks if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) { - // val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) + // val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) /*val builder = BottomSheetDialog(view.context, R.style.AlertDialogCustom) builder.setTitle("Pick source")*/ val bottomSheetDialog = BottomSheetDialog(view.context) @@ -212,7 +213,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi links.add(it) } } - + if (res is Resource.Success) { val sorted = sortUrls(links) if (sorted.isNotEmpty()) { @@ -295,9 +296,9 @@ class ControllerActivity : ExpandedControllerActivity() { uiMediaController.bindViewToUIController(skipBackButton, SkipTimeController(skipBackButton, false)) uiMediaController.bindViewToUIController(skipForwardButton, SkipTimeController(skipForwardButton, true)) uiMediaController.bindViewToUIController(skipOpButton, SkipNextEpisodeController(skipOpButton)) - /* val progressBar: CastSeekBar? = findViewById(R.id.cast_seek_bar) + /* val progressBar: CastSeekBar? = findViewById(R.id.cast_seek_bar) - progressBar?.backgroundTintList = (UIHelper.adjustAlpha(colorFromAttribute(R.attr.colorPrimary), 0.35f)) -*/ + progressBar?.backgroundTintList = (UIHelper.adjustAlpha(colorFromAttribute(R.attr.colorPrimary), 0.35f)) + */ } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt index 73052ab0..51588d58 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt @@ -386,11 +386,12 @@ class PlayerFragment : Fragment() { if (useSystemBrightness) { // https://developer.android.com/reference/android/view/WindowManager.LayoutParams#screenBrightness val lp = activity?.window?.attributes - val currentBrightness = if (lp?.screenBrightness ?: -1.0f <= 0f) (android.provider.Settings.System.getInt( - context?.contentResolver, - android.provider.Settings.System.SCREEN_BRIGHTNESS - ) * (1 / 255).toFloat()) - else lp?.screenBrightness!! + val currentBrightness = + if (lp?.screenBrightness ?: -1.0f <= 0f) (android.provider.Settings.System.getInt( + context?.contentResolver, + android.provider.Settings.System.SCREEN_BRIGHTNESS + ) * (1 / 255).toFloat()) + else lp?.screenBrightness!! val alpha = minOf( maxOf( @@ -403,8 +404,7 @@ class PlayerFragment : Fragment() { progressBarRight?.max = 100 * 100 progressBarRight?.progress = (alpha * 100 * 100).toInt() - } - else { + } else { val alpha = minOf(0.95f, brightness_overlay.alpha + diffY.toFloat() * 0.5f) // 0.05f *if (diffY > 0) 1 else -1 brightness_overlay?.alpha = alpha @@ -638,6 +638,7 @@ class PlayerFragment : Fragment() { private var episodes: List = ArrayList() var currentPoster: String? = null var currentHeaderName: String? = null + var currentIsMovie: Boolean? = null //region PIP MODE private fun getPen(code: PlayerEventType): PendingIntent { @@ -763,6 +764,7 @@ class PlayerFragment : Fragment() { val index = links.indexOf(getCurrentUrl()) context?.startCast( apiName, + currentIsMovie ?: return@addCastStateListener, currentHeaderName, currentPoster, epData.index, @@ -893,6 +895,7 @@ class PlayerFragment : Fragment() { localData = d currentPoster = d.posterUrl currentHeaderName = d.name + currentIsMovie = !d.isEpisodeBased() } } is Resource.Failure -> { @@ -1209,8 +1212,8 @@ class PlayerFragment : Fragment() { } } - private fun handlePauseEvent(pause : Boolean) { - if(pause) { + private fun handlePauseEvent(pause: Boolean) { + if (pause) { handlePlayerEvent(PlayerEventType.Pause) } } @@ -1474,6 +1477,7 @@ class PlayerFragment : Fragment() { } override fun onPlayerError(error: ExoPlaybackException) { + println("CURRENT URL: " + currentUrl.url) // Lets pray this doesn't spam Toasts :) when (error.type) { ExoPlaybackException.TYPE_SOURCE -> { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index c79fe2ab..0667a4af 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -29,6 +29,7 @@ import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.UIHelper.getStatusBarHeight @@ -176,6 +177,7 @@ class ResultFragment : Fragment() { } private var currentPoster: String? = null + private var currentIsMovie: Boolean? = null var url: String? = null @@ -239,22 +241,33 @@ class ResultFragment : Fragment() { currentLoadingCount++ when (episodeClick.action) { ACTION_CHROME_CAST_EPISODE -> { + + val skipLoading = if (apiName != null) { + getApiFromName(apiName).instantLinkLoading + } else false + + var dialog : AlertDialog? = null val currentLoad = currentLoadingCount - val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent) - val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null) - builder.setView(customLayout) - val dialog = builder.create() + if(!skipLoading) { + val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent) + val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null) + builder.setView(customLayout) - dialog.show() - dialog.setOnDismissListener { - currentLoadingCount++ + dialog = builder.create() + + dialog.show() + dialog.setOnDismissListener { + currentLoadingCount++ + } } + // Toast.makeText(activity, "Loading links", Toast.LENGTH_SHORT).show() viewModel.loadEpisode(episodeClick.data, true) { data -> if (currentLoadingCount != currentLoad) return@loadEpisode - dialog.dismiss() + dialog?.dismiss() + when (data) { is Resource.Failure -> { Toast.makeText(activity, "Failed to load links", Toast.LENGTH_SHORT).show() @@ -263,6 +276,7 @@ class ResultFragment : Fragment() { val eps = currentEpisodes ?: return@loadEpisode context?.startCast( apiName ?: return@loadEpisode, + currentIsMovie ?: return@loadEpisode, currentHeaderName, currentPoster, episodeClick.data.index, @@ -354,6 +368,7 @@ class ResultFragment : Fragment() { currentHeaderName = d.name currentPoster = d.posterUrl + currentIsMovie = !d.isEpisodeBased() result_openinbrower.setOnClickListener { val i = Intent(Intent.ACTION_VIEW) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index 164bad76..52e38369 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -104,11 +104,12 @@ class ResultViewModel : ViewModel() { val episodes = ArrayList() for ((index, i) in d.episodes.withIndex()) { episodes.add(context.buildResultEpisode( - null, // TODO ADD NAMES + (i.name + ?: (if (i.season != null && i.episode != null) "S${i.season}:E${i.episode}" else null)), // TODO ADD NAMES null, - index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE - null, // TODO FIX SEASON - i, + i.episode ?: (index + 1), + i.season, + i.data, apiName, (mainId + index + 1).hashCode(), index, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 7a8e2b9d..0f2ba48d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -144,7 +144,9 @@ class SearchFragment : Fragment() { } } } - searchViewModel.search("overlord") + allApi.providersActive = requireActivity().getApiSettings() + + searchViewModel.search("iron man") // (activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro") /* (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index 1cd8abe1..d2dc1077 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.lagradost.cloudstream3.APIHolder.allApi import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.mvvm.Resource @@ -11,15 +12,13 @@ import com.lagradost.cloudstream3.mvvm.safeApiCall import kotlinx.coroutines.launch class SearchViewModel : ViewModel() { - val api: MainAPI = apis[0] //TODO MULTI API - private val _searchResponse: MutableLiveData>> = MutableLiveData() val searchResponse: LiveData>> get() = _searchResponse fun search(query: String) = viewModelScope.launch { _searchResponse.postValue(Resource.Loading()) val data = safeApiCall { - api.search(query) + allApi.search(query) } _searchResponse.postValue(data as Resource>?) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt index 6b4dba2a..29eb4fcd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt @@ -31,7 +31,10 @@ object CastHelper { val link = holder.currentLinks[index] val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, - (epData.name ?: "Episode ${epData.episode}") + " - ${link.name}") + if (holder.isMovie) + link.name + else + (epData.name ?: "Episode ${epData.episode}") + " - ${link.name}") movieMetadata.putString(MediaMetadata.KEY_TITLE, holder.title) @@ -58,7 +61,7 @@ object CastHelper { println("FAILED AND LOAD NEXT") } else -> { - println("FAILED::: " + res.status) + //IDK DO SMTH HERE } } } @@ -67,6 +70,7 @@ object CastHelper { fun Context.startCast( apiName: String, + isMovie: Boolean, title: String?, poster: String?, currentEpisodeIndex: Int, @@ -81,7 +85,7 @@ object CastHelper { val epData = episodes[currentEpisodeIndex] - val holder = MetadataHolder(apiName, title, poster, currentEpisodeIndex, episodes, currentLinks) + val holder = MetadataHolder(apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks) val index = startIndex ?: 0 val mediaItem = @@ -96,7 +100,15 @@ object CastHelper { startTime ?: 0, )) { if (currentLinks.size > index + 1) - startCast(apiName, title, poster, currentEpisodeIndex, episodes, currentLinks, index + 1, startTime) + startCast(apiName, + isMovie, + title, + poster, + currentEpisodeIndex, + episodes, + currentLinks, + index + 1, + startTime) } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 79be3f3e..e388c97e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -15,7 +15,7 @@ data class ExtractorLink( val isM3u8: Boolean = false, ) -fun ExtractorLink.getId() : Int { +fun ExtractorLink.getId(): Int { return url.hashCode() } @@ -27,6 +27,20 @@ enum class Qualities(var value: Int) { UHD(3) // 4k } +fun getQualityFromName(qualityName: String): Int { + return when (qualityName.replace("p", "").replace("P", "")) { + "360" -> Qualities.SD + "480" -> Qualities.SD + "720" -> Qualities.HD + "1080" -> Qualities.FullHd + "1440" -> Qualities.UHD // I KNOW KINDA MISLEADING + "2160" -> Qualities.UHD + "4k" -> Qualities.UHD + "4K" -> Qualities.UHD + else -> Qualities.Unknown + }.value +} + private val packedRegex = Regex("""eval\(function\(p,a,c,k,e,.*\)\)""") fun getPacked(string: String): String? { return packedRegex.find(string)?.value diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 7b175717..be21ed4d 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -248,6 +248,7 @@