Skip to content

Commit 2553ff9

Browse files
ujjol1234david-allison
authored andcommitted
fix(api): cards - list media
Fixed bug which was causing AnkiDroid API to not list media on cards. Due to card rendering being moved to the backend, and Anki using different Regex handling code, we needed to change the implementation of `files_in_str`: [sound:] tags were stripped, for example https://github.com/ankitects/anki/blob/64ca90934bc26ddf7125913abc9dd9de8cb30c2b/pylib/anki/media.py#L136-L150 Fixes 17062
1 parent 923f819 commit 2553ff9

File tree

4 files changed

+115
-11
lines changed

4 files changed

+115
-11
lines changed

AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package com.ichi2.anki.tests
2121
import android.content.ContentResolver
2222
import android.content.ContentUris
2323
import android.content.ContentValues
24+
import android.database.Cursor
2425
import android.database.CursorWindow
2526
import android.net.Uri
2627
import anki.notetypes.StockNotetype
@@ -49,12 +50,15 @@ import com.ichi2.libanki.getStockNotetype
4950
import com.ichi2.libanki.sched.Scheduler
5051
import com.ichi2.testutils.common.assertThrows
5152
import com.ichi2.utils.emptyStringArray
53+
import kotlinx.serialization.json.Json
5254
import net.ankiweb.rsdroid.exceptions.BackendNotFoundException
5355
import org.hamcrest.MatcherAssert.assertThat
56+
import org.hamcrest.Matchers.allOf
5457
import org.hamcrest.Matchers.containsString
5558
import org.hamcrest.Matchers.equalTo
5659
import org.hamcrest.Matchers.greaterThan
5760
import org.hamcrest.Matchers.greaterThanOrEqualTo
61+
import org.hamcrest.Matchers.hasItem
5862
import org.json.JSONObject.NULL
5963
import org.junit.After
6064
import org.junit.Assert.assertEquals
@@ -67,6 +71,7 @@ import org.junit.Rule
6771
import org.junit.Test
6872
import timber.log.Timber
6973
import kotlin.test.assertNotNull
74+
import kotlin.test.assertTrue
7075
import kotlin.test.junit.JUnitAsserter.assertNotNull
7176

7277
/**
@@ -1345,6 +1350,48 @@ class ContentProviderTest : InstrumentedTest() {
13451350
} ?: fail("query returned null")
13461351
}
13471352

1353+
@Test
1354+
fun testMediaFilesAddedCorrectlyInReviewInfo() {
1355+
val imageFileName = "img.jpg"
1356+
val audioFileName = "test.mp3"
1357+
addNoteUsingBasicNoteType("""Hello <img src="$imageFileName"> [sound:$audioFileName]""")
1358+
.firstCard(col)
1359+
.update {
1360+
queue = QueueType.New
1361+
due = col.sched.today
1362+
}
1363+
1364+
queryReviewInfo { cursor ->
1365+
val media =
1366+
cursor
1367+
.getString(cursor.getColumnIndex(FlashCardsContract.ReviewInfo.MEDIA_FILES))
1368+
.let { Json.decodeFromString<List<String>>(it) }
1369+
1370+
assertThat(
1371+
"media files returned",
1372+
media,
1373+
allOf(
1374+
hasItem(imageFileName),
1375+
hasItem(audioFileName),
1376+
),
1377+
)
1378+
}
1379+
}
1380+
1381+
private fun queryReviewInfo(block: (Cursor) -> Unit) {
1382+
contentResolver
1383+
.query(
1384+
FlashCardsContract.ReviewInfo.CONTENT_URI,
1385+
null,
1386+
null,
1387+
null,
1388+
null,
1389+
)?.use { cursor ->
1390+
assertTrue("has rows") { cursor.moveToFirst() }
1391+
block(cursor)
1392+
}
1393+
}
1394+
13481395
@Test
13491396
fun emptyCards_noCardsInCollection() {
13501397
assertThat("deleted when collection empty", emptyCards(notetypes.cloze), equalTo(0))

AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ abstract class InstrumentedTest {
151151
}
152152
}
153153

154+
@DuplicatedCode("This should be refactored into a shared library later")
155+
fun Card.update(block: Card.() -> Unit): Card {
156+
block(this)
157+
col.updateCard(this)
158+
return this
159+
}
160+
154161
@DuplicatedCode("This is copied from RobolectricTest. This will be refactored into a shared library later")
155162
protected fun Card.moveToReviewQueue() {
156163
this.queue = QueueType.Rev

AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1137,7 +1137,7 @@ class CardContentProvider : ContentProvider() {
11371137
FlashCardsContract.ReviewInfo.NEXT_REVIEW_TIMES -> rb.add(nextReviewTimesJson.toString())
11381138
FlashCardsContract.ReviewInfo.MEDIA_FILES ->
11391139
rb.add(
1140-
JSONArray(col.media.filesInStr(currentCard.question(col) + currentCard.answer(col))),
1140+
JSONArray(col.media.filesInStr(currentCard.renderOutput(col))),
11411141
)
11421142
else -> throw UnsupportedOperationException("Queue \"$column\" is unknown")
11431143
}

AnkiDroid/src/main/java/com/ichi2/libanki/Media.kt

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ package com.ichi2.libanki
2020
import androidx.annotation.WorkerThread
2121
import anki.media.CheckMediaResponse
2222
import com.google.protobuf.kotlin.toByteString
23+
import com.ichi2.libanki.Media.Companion.htmlMediaRegexps
24+
import com.ichi2.libanki.TemplateManager.TemplateRenderContext.TemplateRenderOutput
2325
import com.ichi2.libanki.exception.EmptyMediaException
26+
import com.ichi2.libanki.utils.LibAnkiAlias
27+
import com.ichi2.libanki.utils.NotInLibAnki
2428
import timber.log.Timber
2529
import java.io.File
2630

@@ -63,18 +67,36 @@ open class Media(
6367
/**
6468
* Extract media filenames from an HTML string.
6569
*
66-
* @param string The string to scan for media filenames ([sound:...] or <img...>).
67-
* @return A list containing all the sound and image filenames found in the input string.
70+
* @param currentCard The card to scan for media filenames ([sound:...] or <img...>).
71+
* @return A distinct, unordered list containing all the sound and image filenames found in the card.
6872
*/
69-
fun filesInStr(string: String): List<String> =
70-
col.backend
71-
.extractAvTags(string, true)
72-
.avTagsList
73-
.filter {
74-
it.hasSoundOrVideo()
75-
}.map {
76-
it.soundOrVideo
73+
@LibAnkiAlias("files_in_str")
74+
fun filesInStr(
75+
renderOutput: TemplateRenderOutput,
76+
includeRemote: Boolean = false,
77+
): List<String> {
78+
val files = mutableListOf<String>()
79+
val processedText = LaTeX.mungeQA(renderOutput.questionText + renderOutput.answerText, col, true)
80+
81+
for (pattern in htmlMediaRegexps) {
82+
val matches = pattern.findAll(processedText)
83+
for (match in matches) {
84+
val fname = pattern.extractFilename(match) ?: continue
85+
val isLocal = !Regex("(?i)https?|ftp://").containsMatchIn(fname)
86+
if (isLocal || includeRemote) {
87+
files.add(fname)
88+
}
7789
}
90+
}
91+
92+
// not in libAnki: the rendered output no longer contains [sound:] tags
93+
files.addAll(
94+
(renderOutput.questionAvTags + renderOutput.answerAvTags)
95+
.filterIsInstance<SoundOrVideoTag>()
96+
.map { it.filename },
97+
)
98+
return files.distinct()
99+
}
78100

79101
fun findUnusedMediaFiles(): List<File> = check().unusedList.map { File(dir, it) }
80102

@@ -134,4 +156,32 @@ open class Media(
134156
private fun restoreTrash() {
135157
col.backend.restoreTrash()
136158
}
159+
160+
companion object {
161+
/**
162+
* Given a [media regex][htmlMediaRegexps] and a match, return the captured media filename
163+
*/
164+
@NotInLibAnki
165+
private fun Regex.extractFilename(match: MatchResult): String? {
166+
val index =
167+
when (htmlMediaRegexps.indexOf(this)) {
168+
0, 2 -> 3
169+
1, 3 -> 2
170+
else -> throw IllegalStateException(pattern)
171+
}
172+
return match.groups[index]?.value
173+
}
174+
175+
val htmlMediaRegexps =
176+
listOf(
177+
// src element quoted case (img/audio)
178+
Regex("(?i)(<(?:img|audio)\\b[^>]* src=(['\"])([^>]+?)\\2[^>]*>)"), // Group 3 = fname
179+
// unquoted src (img/audio)
180+
Regex("(?i)(<(?:img|audio)\\b[^>]* src=(?!['\"])([^ >]+)[^>]*?>)"), // Group 2 = fname
181+
// quoted data attribute (object)
182+
Regex("(?i)(<object\\b[^>]* data=(['\"])([^>]+?)\\2[^>]*>)"), // Group 3 = fname
183+
// unquoted data attribute (object)
184+
Regex("(?i)(<object\\b[^>]* data=(?!['\"])([^ >]+)[^>]*?>)"), // Group 2 = fname
185+
)
186+
}
137187
}

0 commit comments

Comments
 (0)