[Android] Youtube Data API - 동영상 검색(Oauth, Api key)

과거 유튜브 알람앱을 만들때에는 유튜브 동영상의 공유를 통해 아이디를 받아와서 사용하도록 했었지만

새롭게 출시할 앱은 앱 내에서 검색 기능도 추가할 겸 Youtube Data Api를 사용하는 방법을 알아보았다.

해당 기능은 물론 Retrofit과 같은 HTTP 통신을 통해서도 검색이 가능하지만 Quick Starter 를 기반으로 작성했다.

Youtube Data API

  • Youtube 웹사이트에서 일반적으로 실행되는 기능을 웹 혹은 앱에서 사용할 수 있다.
  • 오늘 사용할 것은 그중에서도 컨텐츠 검색이다.

선행 작업

1. Youtube Data Api 사용 설정

  • [링크]를 통해 GCP console로 이동해서 프로젝트를 확인하고 사용 설정을 해준다.

    Untitled

2 - 1 - 1.[ OAUTH 동의를 이용하는 경우 ] OAUTH 동의 화면 등록

  • Oauth 동의 화면을 등록해 준다. (해당 예제는 기능 테스트만을 위한 것입니다.)

    Untitled

    Untitled

    Untitled

2 - 1 - 2. 사용자 인증 정보 추가

  • 페이지 좌측 [ 사용 설정된 API 및 서비스 ] → 목록 중 [ Youtube Data Api V3 ]
  • 사용자 인증정보 탭에서 [ 사용자 인증 정보 만들기 ] → [ OAuth 클라이언트 ID ] 클릭

    Untitled

  • 이후 패키지 이름 및 SHA1을 등록해준다. [ 참고 ]

    Untitled

  • 테스트 앱게시 클릭

    Untitled

2 - 2.[ API KEY를 이용하는 경우 ]

  • 페이지 좌측 [ 사용 설정된 API 및 서비스 ] → 목록 중 [ Youtube Data Api V3 ]
  • 사용자 인증정보 탭에서 [ 사용자 인증 정보 만들기 ] → [ API 키 ] 클릭

    Untitled

  • Android 제한 사항에 패키지, SHA1 추가 후 API KEY 사용

    Untitled

1. 라이브러리 추가

  • 각 라이브러리 별 최신 버전은 https://mvnrepository.com/ 참조
dependencies {
	...
	implementation("com.google.android.gms:play-services-auth:20.7.0")
	implementation("com.google.apis:google-api-services-youtube:v3-rev222-1.25.0")
	implementation("com.google.api-client:google-api-client-android:2.2.0")
	...
}
  • 2 files found with path 'META-INF/DEPENDENCIES'. 에러 발생 시 아래 구문 추가

      android {
      	...
      	packaging {
      		resources.excludes.add("META-INF/DEPENDENCIES")
      	}
      	...
      }
    

2. 권한 추가

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />

3. 샘플 코드 1 - OAUTH 인증 사용 예시


class MainActivity : AppCompatActivity() {
    companion object {
        const val PREF_NAME = "PREF_NAME"
        const val KEY_SELECTED_NAME = "KEY_SELECTED_NAME"
    }

    private lateinit var _etSearch: EditText
    private lateinit var _btnSearch: Button
    private lateinit var _tvResult: TextView

    private val searchText
        get() = _etSearch.text.toString()

    private val _credential: GoogleAccountCredential by lazy {
        GoogleAccountCredential.usingOAuth2(
            applicationContext,
            listOf(YouTubeScopes.YOUTUBE_READONLY),
        ).setBackOff(ExponentialBackOff())
    }

    private val _credentialLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult(), ::credentialCallBack
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        _etSearch = findViewById(R.id.et_search)
        _btnSearch = findViewById(R.id.btn_search)
        _tvResult = findViewById(R.id.tv_result)

        _btnSearch.setOnClickListener {
            search()
        }
    }

    private fun search() {
        if (checkSignIn()) {
            lifecycleScope.launch {
                val result = youtubeSearch(searchText).joinToString("\n") { it.snippet.title }
                _tvResult.text = result
            }
        }
    }

    private fun checkSignIn(): Boolean {
        val lastSelectedAccountName = getSharedPreferences(PREF_NAME, MODE_PRIVATE)
            .getString(KEY_SELECTED_NAME, null)
        return if (lastSelectedAccountName.isNullOrBlank()) {
            val signInIntent = _credential.newChooseAccountIntent()
            _credentialLauncher.launch(signInIntent)
            false
        } else {
            _credential.selectedAccountName = lastSelectedAccountName
            true
        }
    }

    private fun credentialCallBack(result: ActivityResult) {
        val accountName = result.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) ?: return

        getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit {
            putString(KEY_SELECTED_NAME, accountName)
        }
        _credential.selectedAccountName = accountName

        Toast.makeText(this, "검색 가능", Toast.LENGTH_SHORT).show()
    }

    private suspend fun youtubeSearch(query: String): List<SearchResult> {
        val transport = NetHttpTransport()
        val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
        val youtube = YouTube.Builder(
            transport, jsonFactory, _credential
        ).setApplicationName(getString(R.string.app_name))
            .build()

        return withContext(Dispatchers.IO) {
            runCatching {
                // 매개변수 참고 [https://developers.google.com/youtube/v3/docs/search/list?hl=ko#parameters]
                val parts = listOf("id", "snippet").joinToString()
                val searchList =
                    youtube.search().list(parts).apply {
                        q = query
                        type = "video"
                        // 음악 카테고리 참고 [https://gist.github.com/dgp/1b24bf2961521bd75d6c]
                        videoCategoryId = "10"
                    }
                val response = searchList?.execute()
                response?.items ?: listOf()
            }.recover { throwable ->
                if (throwable is UserRecoverableAuthIOException) {
                    launch(Dispatchers.Main) {
                        Toast.makeText(this@MainActivity, "인증이 필요합니다.", Toast.LENGTH_SHORT).show()
                        // 엑세스 권한이 없는 경우 권한 요청을 위한 Intent
                        val accessRequestIntent = throwable.intent
                        _credentialLauncher.launch(accessRequestIntent)
                    }
                }
                listOf()
            }.getOrNull() ?: listOf()
        }
    }

}
  • GoogleAccountCrecdential 을 통해 Google 계정에 대한 승인 및 계정 선택을 관리한다.
  • GoogleAccountCrecdential.newChooseAccountIntent() 메서드를 통해 계정 선택이 가능하다.
    • 계정 선택만으로 Youtube 검색은 불가능하다.
    • 선택한 계정이 액세스 권한이 있어야한다.
  • 검색에 대한 예외로 액세스 권한이 없는경우 UserRecoverableAuthIOException 예외가 발생하며, 해당 예외에서 액세스 권한 요청이 가능한 intent를 가져올 수 있다.

3. 샘플코드 2 - API KEY 사용하기

class MainActivity : AppCompatActivity() {
    companion object {
        const val YOUTUBE_DATA_API_KEY = "Youtube Data API 키"
    }

    private lateinit var _etSearch: EditText
    private lateinit var _btnSearch: Button
    private lateinit var _tvResult: TextView

    private val searchText
        get() = _etSearch.text.toString()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        _etSearch = findViewById(R.id.et_search)
        _btnSearch = findViewById(R.id.btn_search)
        _tvResult = findViewById(R.id.tv_result)

        _btnSearch.setOnClickListener {
            search()
        }
    }

    private fun search() {
        lifecycleScope.launch {
            val result = youtubeSearch(searchText).joinToString("\n") { it.snippet.title }
            _tvResult.text = result
        }
    }

    private suspend fun youtubeSearch(query: String): List<SearchResult> {
        val transport = NetHttpTransport()
        val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
        val youtube = YouTube.Builder(
            transport, jsonFactory,
        ) {}.setApplicationName(getString(R.string.app_name))
            .build()

        return withContext(Dispatchers.IO) {
            runCatching {
                // 매개변수 참고 [https://developers.google.com/youtube/v3/docs/search/list?hl=ko#parameters]
                val parts = listOf("id", "snippet").joinToString()
                val searchList =
                    youtube.search().list(parts).apply {
                        key = YOUTUBE_DATA_API_KEY
                        q = query
                        type = "video"
                        // 음악 카테고리 참고 [https://gist.github.com/dgp/1b24bf2961521bd75d6c]
                        videoCategoryId = "10"
                    }
                val response = searchList?.execute()
                response?.items ?: listOf()
            }.recover {
                it.printStackTrace()
                listOf()
            }.getOrElse { listOf() }
        }
    }

}
  • 유저에게 액세스 권한을 물을 필요 없이 API KEY를 이용하면 된다.

깃헙에서 확인하기.

  • 해당 예제에 대한 소스는 [ 링크 ] 를 통해 확인이 가능합니다.
  • OAuth, API Key 사용은 각각 브랜치를 확인해주세요.

사용 후기

  • OAUTH를 통한다면 권한을 통해 검색뿐아니라 보다 다양한 작업이 가능할 것으로 보인다.
  • 각 API 별 할당량 이 있는데 그 중 검색은 하루에 100번 검색이면 할당량을 다쓰는 만큼 굉장히 크다.
  • 처음 API KEY로만 사용할 때와 OATUH를 통한 검색의 할당량의 차이가 있을까 싶었지만 둘이 같은것으로 보여진다.

참고 자료


Youtube Data Api